Severity: Critical (CVSS ~9.8)
Summary
The owner identity that scopes all data access (tasks, findings, reports, vault secrets, credential profiles) is derived entirely from the X-User-Id request header — a value the client controls with no server-side verification. Any client possessing the deployment's shared API key can exfiltrate every other workspace's encrypted vault secrets, tasks, findings, and reports.
Vulnerable Code
backend/secuscan/auth.py:239-245 — Owner resolved from client-controlled header:
_OWNER_HEADER = "x-user-id"
def resolve_owner_id(request: Request | None) -> str:
"""Resolve the owning user/workspace identity for the current request."""
if request is not None:
user_id = request.headers.get(_OWNER_HEADER)
if user_id and user_id.strip():
return f"user:{user_id.strip()}"
return DEFAULT_OWNER_ID
backend/secuscan/routes.py:1616-1640 — Vault secrets returned based on spoofed owner:
@router.get("/vault/{name}", dependencies=[Depends(vault_limiter)])
async def get_vault_secret(
name: str,
owner: str = Depends(get_current_owner), # <-- Derived from X-User-Id
):
db = await get_db()
row = await db.fetchone(
"SELECT encrypted_value FROM credential_vault WHERE owner_id = ? AND name = ?",
(owner, name), # <-- Queried with spoofed owner
)
if not row:
raise HTTPException(status_code=404, detail="Secret not found")
crypto = VaultCrypto(settings.resolved_vault_key)
return {
"name": name,
"value": crypto.decrypt(row["encrypted_value"]), # <-- Decrypted secret returned
}
Attack Vector
GET /api/v1/vault/admin-password HTTP/1.1
Host: secuscan.example.com
X-Api-Key: <shared-deployment-key>
X-User-Id: victim-workspace
The server decrypts and returns the victim workspace's admin-password secret in plaintext.
Impact
- Full credential theft: Encrypted vault secrets (passwords, tokens, session cookies) decrypted and returned to attacker
- Cross-workspace data access: Tasks, findings, reports, credential profiles — all scoped by spoofed owner
- Silent exploitation: Audit log records the spoofed
X-User-Id as legitimate — no anomaly flagged
- Affects all data categories: Every query uses the
owner_id derived from this header
Affected Endpoints
All endpoints using Depends(get_current_owner):
GET /vault/{name} — Decrypt and return vault secrets
POST /vault — Store secrets in another workspace
DELETE /vault/{name} — Delete another workspace's secrets
- All task, finding, report, and credential profile queries
Recommended Fix
- Remove client-delegated ownership for vault/credential endpoints
- Bind
owner_id to a verified session or cryptographic identity, not a header
- For multi-user deployments: require
X-User-Id only from trusted upstream proxy with signature/IP validation
- Add per-owner encryption keys so cross-owner decryption fails even if ownership is confused
- Strip
X-User-Id from external requests at the reverse proxy edge
Severity: Critical (CVSS ~9.8)
Summary
The owner identity that scopes all data access (tasks, findings, reports, vault secrets, credential profiles) is derived entirely from the
X-User-Idrequest header — a value the client controls with no server-side verification. Any client possessing the deployment's shared API key can exfiltrate every other workspace's encrypted vault secrets, tasks, findings, and reports.Vulnerable Code
backend/secuscan/auth.py:239-245— Owner resolved from client-controlled header:backend/secuscan/routes.py:1616-1640— Vault secrets returned based on spoofed owner:Attack Vector
The server decrypts and returns the victim workspace's
admin-passwordsecret in plaintext.Impact
X-User-Idas legitimate — no anomaly flaggedowner_idderived from this headerAffected Endpoints
All endpoints using
Depends(get_current_owner):GET /vault/{name}— Decrypt and return vault secretsPOST /vault— Store secrets in another workspaceDELETE /vault/{name}— Delete another workspace's secretsRecommended Fix
owner_idto a verified session or cryptographic identity, not a headerX-User-Idonly from trusted upstream proxy with signature/IP validationX-User-Idfrom external requests at the reverse proxy edge