Skip to content

CRITICAL: Broken Object-Level Authorization (BOLA) via client-spoofable X-User-Id header enables cross-workspace credential theft #2021

Description

@namann5

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

  1. Remove client-delegated ownership for vault/credential endpoints
  2. Bind owner_id to a verified session or cryptographic identity, not a header
  3. For multi-user deployments: require X-User-Id only from trusted upstream proxy with signature/IP validation
  4. Add per-owner encryption keys so cross-owner decryption fails even if ownership is confused
  5. Strip X-User-Id from external requests at the reverse proxy edge

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:backendBackend API, database, or service workarea:securitySecurity-sensitive implementation or testslevel:critical80 pts difficulty label for critical or high-impact PRspriority:highHigh-priority issuetype:securitySecurity work category bonus label

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions