17 KiB
Authorization Analysis Report
1. Executive Summary
- Analysis Status: Complete
- Key Outcome: Multiple critical authorization vulnerabilities discovered across horizontal IDOR, vertical privilege escalation, and permission management. All high-confidence findings have been passed to the exploitation phase via the machine-readable exploitation queue.
- Purpose of this Document: This report provides the strategic context, dominant patterns, and architectural intelligence necessary to effectively exploit the vulnerabilities listed in the queue. It is intended to be read alongside the JSON deliverable.
Critical Findings Overview:
- 10 High-Severity Horizontal IDOR vulnerabilities - Users can access other users' complete health records, medical images, and audit logs
- 3 Critical Vertical Escalation vulnerabilities - Users with read-only access can write data; users can self-escalate permissions
- 1 Permission Management bypass - Users with PermWrite can delete files without PermDelete permission
Root Cause: The application uses stub library functions that completely ignore AccessContext parameters, creating a single layer of defense at the web/API layer. When this layer is missing or incomplete, no secondary authorization exists.
2. Dominant Vulnerability Patterns
Pattern 1: Stub Functions Ignore AccessContext (Horizontal + Vertical)
Description: The application's core library in /repos/inou-portal/lib/stubs.go contains stub implementations that accept AccessContext parameters for authorization but completely ignore them, directly querying the database without any permission checks.
Affected Functions:
lib.DossierGet(ctx, id)- Ignoresctx, directly callsentryQuery()(lines 196-218)lib.EntryQuery(ctx, ...)- Ignoresctx, directly callsentryQuery()(lines 92-102)lib.EntryList(accessorID, ...)- IgnoresaccessorID, directly callsentryQuery()(lines 104-113)lib.EntryGet(ctx, id)- Stub returns "not implemented" (lines 87-90)lib.AuditList(filter)- No AccessContext parameter at all (lines 480-483)
Implication: Any endpoint that relies solely on these stub functions for authorization has no actual access control. The web/API layer must enforce all authorization, creating a single point of failure.
Representative Vulnerabilities:
- AUTHZ-VULN-01: GET /dossier/{id}/export bypasses authorization
- AUTHZ-VULN-02: GET /api/v1/dossiers/{id} bypasses authorization
- AUTHZ-VULN-03: GET /api/v1/dossiers/{id}/entries bypasses authorization
- AUTHZ-VULN-08: GET /dossier/{id}/audit bypasses authorization
Code Evidence:
// /repos/inou-portal/lib/stubs.go:196-218
func DossierGet(ctx *AccessContext, id string) (*Dossier, error) {
entries, err := entryQuery(id, &Filter{Category: 0}) // ctx parameter IGNORED
if err != nil || len(entries) == 0 {
return nil, fmt.Errorf("dossier not found: %s", id)
}
// ... returns data without any authorization check
}
Pattern 2: Passing nil or SystemAccessorID Bypasses All Checks (Horizontal)
Description: Several endpoints explicitly pass nil as the AccessContext or use lib.SystemAccessorID, which causes the RBAC system in lib.CheckAccess() to immediately return true, bypassing all permission checks.
Bypass Mechanism:
// /repos/inou-portal/lib/rbac.go:19-24
func CheckAccess(accessorID, dossierID, entryID string, perm int) bool {
if accessorID == "" || accessorID == SystemAccessorID {
return true // BYPASS ALL CHECKS
}
// ... actual permission checks
}
Affected Endpoints:
GET /contact-sheet.webp/{id}- PassesniltoEntryGet(nil, series.ParentID)andDossierGet(nil, dossierID)GET /api/v1/images/{id}- PassesniltoEntryGet(nil, id)andImageGet(nil, id, opts)GET /api/v1/dossiers/{id}/entries- Useslib.SystemAccessorIDforEntryList(SystemAccessorID, ...)
Representative Vulnerabilities:
- AUTHZ-VULN-05: GET /contact-sheet.webp/{id} passes nil context
- AUTHZ-VULN-06: GET /api/v1/images/{id} passes nil context
- AUTHZ-VULN-03: GET /api/v1/dossiers/{id}/entries uses SystemAccessorID
Pattern 3: Missing Write Permission Checks (Vertical Escalation)
Description: Write and modify endpoints fail to validate PermWrite permission before performing state-changing operations, allowing users with only PermRead (view-only) access to create, update, or delete data.
Implication: Users granted read-only access to view a family member's health records can modify that data, violating the principle of least privilege.
Representative Vulnerabilities:
- AUTHZ-VULN-11: POST /dossier/{id}/trackers/respond - No authorization check at all
- AUTHZ-VULN-12: POST /api/entries - No PermWrite validation before create/update/delete
Code Evidence:
// /repos/inou-portal/portal/trackers.go:348-399
func handleTrackerRespond(w http.ResponseWriter, r *http.Request) {
p := getLoggedInDossier(r) // Only checks authentication
if p == nil {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// NO CHECK for PermWrite on targetHex!
// Directly allows POST to API without authorization
}
Pattern 4: Permission Escalation via Self-Granting (Vertical Escalation)
Description: The permission management endpoint allows users with PermManage to grant permissions to themselves, and to grant permissions they don't possess, enabling privilege escalation.
Implication: A user with limited PermManage access (e.g., on a child's health record) can grant themselves PermDelete to erase medical history, without proper oversight.
Representative Vulnerabilities:
- AUTHZ-VULN-13: POST /dossier/{id}/permissions allows self-escalation
- AUTHZ-VULN-14: DELETE /dossier/{id}/files/{fileId}/delete allows PermWrite users to delete
Code Evidence:
// /repos/inou-portal/portal/main.go:1403-1442
// No check that grantee.DossierID != p.DossierID
// No validation that ops_to_grant ⊆ grantor_current_ops
if role == "custom" {
if r.FormValue("op_w") == "1" { ops |= lib.PermWrite }
if r.FormValue("op_d") == "1" { ops |= lib.PermDelete } // Can grant PermDelete without having it
if r.FormValue("op_m") == "1" { ops |= lib.PermManage }
}
3. Strategic Intelligence for Exploitation
Session Management Architecture
Session Token Storage:
- Portal uses cookie named
logincontaining plaintext dossier ID (16-char hex) - Cookie attributes:
HttpOnly=true, Secure=true, SameSite=Lax - Mobile API uses session tokens stored in
Dossier.SessionTokenfield (64-char hex, never expires) - OAuth uses encrypted access tokens (15-minute expiry) with AES-256-GCM
Critical Finding: The login cookie value is the dossier ID itself, making it predictable if the ID generation algorithm is weak. Session tokens for mobile API never expire, creating long-lived access.
Token Extraction Points:
- Portal:
getLoggedInDossier(r)readslogincookie at/repos/inou-portal/portal/main.go:279-285 - API:
getAccessContextOrFail(w, r)reads Bearer token fromAuthorizationheader at/repos/inou-portal/api/auth.go:76-86 - V1 API:
v1GetAuth(r)extracts from Bearer token orX-API-Tokenheader at/repos/inou-portal/api/api_v1.go:27-59
Role/Permission Model
Permission Bitmask System:
- PermRead = 1 (bit 0) - View dossier data
- PermWrite = 2 (bit 1) - Modify dossier data
- PermDelete = 4 (bit 2) - Delete entries
- PermManage = 8 (bit 3) - Grant access to others
Permission Storage:
- Database table:
access(columns: AccessorID, DossierID, GranteeID, EntryID, Relation, Ops, CreatedAt) - Encrypted at rest with AES-256-GCM
- No caching - permissions checked on every request via database query
Critical Finding:
- Self-access (when
accessorID == dossierID) grants implicit full permissions without database lookup SystemAccessorID(value:7b3a3ee1c2776dcd) bypasses all RBAC checks- Empty string
""as accessorID also bypasses all checks - Localhost requests (127.0.0.1 or ::1) automatically get SystemContext
Permission Check Functions:
- Core RBAC:
CheckAccess(accessorID, dossierID, entryID, perm)at/repos/inou-portal/lib/rbac.go:19-54 - V1 API:
v1CanAccess(authID, targetID)at/repos/inou-portal/api/api_v1.go:61-70(checks ANY access grant exists) - V1 API:
v1CanWrite(authID, targetID)at/repos/inou-portal/api/api_v1.go:72-86(checks PermWrite bit) - Portal:
lib.CanManageDossier(accessorID, dossierID)at/repos/inou-portal/lib/rbac.go:56-58(checks PermManage bit)
Resource Access Patterns
Dossier ID Format:
- 16-character hexadecimal string (e.g.,
a1b2c3d4e5f67890) - Generated via
lib.NewID()usingcrypto/rand- cryptographically random - However: Demo dossier uses hardcoded ID
1111111111111111
Entry ID Format:
- Same 16-character hex format as dossier IDs
- Used for medical records, files, imaging studies, journal entries
Critical Finding: While IDs are cryptographically random, they are exposed in:
- URL paths (e.g.,
/dossier/{id}) - API responses (e.g., list endpoints return entry IDs)
- Redirect URLs after login
- Error messages may leak IDs through timing attacks
Database Access Pattern:
- Most vulnerable endpoints call
entryQuery(dossierID, filter)which directly executes:
SELECT EntryID, DossierID, ParentID, Category, Type, Value, Summary, Ordinal,
Timestamp, TimestampEnd, Status, Tags, Data, SearchKey
FROM entries WHERE DossierID = ?
- No additional WHERE clauses for authorization - relies on application layer
Localhost Bypass Mechanism
Critical System-Level Bypass:
// /repos/inou-portal/api/auth.go:151-156
func systemContextForLocalhost(r *http.Request) *lib.AccessContext {
if isLocalhost(r) {
return lib.SystemContext // Full system access
}
return nil
}
func isLocalhost(r *http.Request) bool {
ip := strings.Split(r.RemoteAddr, ":")[0]
return ip == "127.0.0.1" || ip == "[::1]"
}
Implication: Any request from localhost bypasses ALL authorization checks. This is used by:
/api/accessendpoint (localhost only)/api/entriesendpoint (callsgetAccessContextOrSystem)- Internal service-to-service calls
Exploitation Note: If X-Forwarded-For or similar proxy headers are trusted without validation, remote attackers could spoof localhost IP to gain system access.
Workflow Implementation
Authentication Flow:
- User submits email → 6-digit code sent
- User submits code → validated (or backdoor code
250365always works) - Session cookie created with dossier ID
- Redirect to /dashboard or /onboard
Critical Finding: Hardcoded backdoor verification code 250365 exists in production:
- Location:
/repos/inou-portal/lib/dbcore.go:347(portal),/repos/inou-portal/portal/api_mobile.go:128(mobile API) - Impact: Anyone can authenticate as any email address without actually receiving the verification code
File Upload → Processing Flow:
- Upload file → Entry created with status="uploaded"
- Call
/dossier/{id}/process-imaging→ Status changed to "processing" - External DICOM binary processes files
- Status updated to "done"
Critical Finding: Files can be processed multiple times if endpoint called concurrently (no locking mechanism). Status transitions are not atomic.
4. Vectors Analyzed and Confirmed Secure
These authorization checks were traced and confirmed to have robust, properly-placed guards. They are low-priority for further testing.
| Endpoint | Guard Location | Defense Mechanism | Verdict |
|---|---|---|---|
GET /dossier/{id} |
/repos/inou-portal/lib/dbcore.go:127 |
lib.EntryRead enforces PermRead BEFORE database query via CheckAccess | SAFE |
POST /onboard |
/repos/inou-portal/portal/main.go:594-596 |
Requires valid login cookie (email verification completed) | SAFE |
POST /oauth/token |
/repos/inou-portal/portal/oauth.go:144-296 |
OAuth 2.0 flow with PKCE validation, single-use codes, expiry checks | SAFE |
GET /api/v1/dossiers/{id}/journal/{entryId} |
/repos/inou-portal/lib/journal.go:195-197 |
lib.GetJournal validates entry.DossierID matches requested dossierID | SAFE |
POST /api/v1/dossiers/{id}/journal |
/repos/inou-portal/api/api_v1.go:810-813 |
v1CanWrite validates PermWrite via access table query | SAFE |
PATCH /api/v1/dossiers/{id}/journal/{entryId} |
/repos/inou-portal/api/api_v1.go:870-873 |
v1CanWrite validates PermWrite before update | SAFE |
POST /dossier/{id}/edit |
/repos/inou-portal/portal/main.go:986-996 |
Checks CanEdit (PermWrite) via getAccess before allowing profile edits | SAFE |
POST /dossier/{id}/upload |
/repos/inou-portal/portal/upload.go:166-172 |
Checks CanEdit (PermWrite) via getAccess before allowing file uploads | SAFE |
Note on "SAFE" Verdicts: These endpoints enforce authorization at the web/API layer. While they lack defense-in-depth (the underlying lib functions are stubs or ignore context), the primary authorization layer is correctly implemented and sufficient to block unauthorized access under normal conditions.
5. Analysis Constraints and Blind Spots
Limitations Encountered During Analysis
1. Stub Function Implementations:
- Multiple critical library functions (
DossierGet,EntryGet,EntryQuery,EntryList) are marked as stubs with comments like[STUB]or "needs migration to EntryRead" - These stubs either return errors or ignore security parameters
- Impossible to determine if future implementations will maintain current security posture
- Impact: Some vulnerabilities may be mitigated when stubs are replaced with proper implementations
2. External Service Authorization:
- The application makes HTTP calls to internal services (e.g.,
http://localhost:8082/api/*) - Authorization for these backend APIs was analyzed, but the network boundary between portal and API server could not be verified
- Assumption: Internal API endpoints are only accessible from localhost (not directly from internet)
- Impact: If internal APIs are exposed, additional attack surface exists
3. Concurrent Access and Race Conditions:
- Static code analysis cannot definitively identify race conditions in permission checks
- Example: File processing status transitions are not atomic
- Example: Permission grants could have TOCTOU vulnerabilities if checked and used separately
- Impact: Some timing-based bypasses may exist but were not confirmed
4. Dynamic Permission System:
- The
accesstable can be modified at runtime to grant/revoke permissions - The analysis assumes permissions are relatively static during a request
- If permissions change mid-request, behavior is undefined
- Impact: Complex multi-step workflows may have consistency issues
5. OAuth Client Registration:
- OAuth clients can be dynamically registered via
/registerendpoint - Analysis focused on authorization, not OAuth client validation
- Malicious client registration could potentially bypass intended access patterns
- Impact: OAuth-specific attacks (client impersonation, redirect URI manipulation) were not fully explored
6. Encrypted Data Access:
- All database data and uploaded files are encrypted with AES-256-GCM
- Authorization analysis assumes encryption keys are properly protected
- If master key at
/tank/inou/master.keyis compromised, all data is exposed regardless of authorization - Impact: Physical/infrastructure security is out of scope but critical
7. Localhost Trust Boundary:
- Many endpoints trust requests from 127.0.0.1/::1 as "system" access
- Analysis assumes no proxy header spoofing is possible
- If
X-Forwarded-For,X-Real-IP, or similar headers are trusted, localhost bypass may be remotely exploitable - Impact: Need to verify reverse proxy configuration in production
Areas Not Fully Analyzed
GraphQL Endpoints:
- Reconnaissance report mentions potential GraphQL, but no GraphQL-specific endpoints were found in the codebase analyzed
- If GraphQL exists, it may have separate authorization logic
WebSocket/SSE Connections:
- Real-time features may use WebSockets or Server-Sent Events
- Authorization for long-lived connections was not analyzed
- Permission changes during active connections may not be enforced
MCP (Model Context Protocol) Authorization:
- The
/mcpendpoint provides AI assistant access via OAuth - MCP-specific tools and prompts have their own authorization
- This analysis focused on dossier/entry access, not MCP-specific operations
Background Job Authorization:
- File processing, genome parsing, and LLM queries run asynchronously
- Authorization context for background jobs was not fully traced
- Jobs may retain elevated permissions even after user permissions are revoked