Skip to content

appsec: BOLA/authz audit — 2026-06-28 #18

Description

@fuzeone-bot

appsec: BOLA / authorization audit — backend FastAPI routers

Scope: backend/app/routers/* mounted in backend/app/main.py. Read-only review against the endpoint-authorization checklist (authN coverage, object-level authz / BOLA-IDOR, field-level / mass-assignment, pydantic validation). FuzeKeys stores secrets, credentials, and PII (identities), so a missing object-level check is high/critical severity.

Reviewer: appsec-reviewer (independent). Fix owner: backend-engineer. This issue does not implement fixes.

Note: governance/architecture-guidelines.md referenced by the review task is not present in the repo at master. Review was performed against the canonical endpoint-authorization checklist plus the reference auth model (per-resource ownership: resource.user_id == current_user.id). Recommend adding the guidelines doc so gate-authz has a declared contract.


Findings by severity

CRITICAL-1 — google_integration.py: no authN + BOLA on every route (cross-tenant PII disclosure and account creation)

File: backend/app/routers/google_integration.py (mounted at /api/google)

Every route takes identity_id (or returns account data) and looks it up by raw id with no get_current_user dependency and no ownership check:

  • POST /api/google/signup/{identity_id}db.query(Identity).filter(Identity.id == identity_id) then creates a real Google account from that identity's PII. Any unauthenticated caller can drive signup using any user's identity.
  • GET /api/google/accounts/{identity_id} — returns account email, status, metadata for any identity (IDOR read).
  • POST /api/google/test/identity-conversion/{identity_id} — returns decrypted PII (first/last name, username, phone_number, recovery_email, birth_date, gender) for any identity. Unauthenticated PII exfiltration.
  • POST /api/google/signup/manual — unauthenticated; drives signup automation from request body.

Fix: add current_user: User = Depends(get_current_user) to every route, and scope every identity/account lookup to the caller, e.g. select(Identity).where(Identity.id == identity_id, Identity.user_id == current_user.id) (mirror accounts.py / identities.py). Return 404 on no-match (do not leak existence). Add a non-owner to 403/404 test.

CRITICAL-2 — llm_scraper.py: no authN on any route (public-by-default; destructive plus cost-bearing)

File: backend/app/routers/llm_scraper.py (mounted at /api/llm-scraper)

No route has any auth dependency. Notably:

  • POST /generate, POST /improve, POST /debug — unauthenticated, invoke the LLM with attacker-controlled input (cost/abuse, prompt-injection surface).
  • DELETE /scrapers/{site_name}/{action_type}any unauthenticated caller can delete scrapers and all versions.
  • GET /scrapers, GET /scrapers/{...}, GET /scrapers/{...}/history, GET /stats — unauthenticated read of generated scraper source/metadata.

Fix: require the application JWT (Depends(get_current_user)) — or the service-key model used by credentials.py — on all mutating and read routes (a /health may stay open). Add per-route auth and an unauthenticated to 401 test. If scrapers are owned, add an ownership column plus scoped filter.

HIGH-1 — site_integrations.py: no authN on operational routes

File: backend/app/routers/site_integrations.py (mounted at /api/v1/integrations)

POST /signup, POST /signin, POST /apikey accept raw email plus password and drive browser automation against external sites (permit.io) unauthenticated. No internal resource id (so not strictly BOLA), but a missing-authN/abuse finding: anyone can drive credential-bearing automation and resource exhaustion. GET /sites, /sites/{site_name}/capabilities are lower risk but also open.

Fix: add Depends(get_current_user) to the signup/signin/apikey operations. Validation is present (pydantic plus EmailStr); keep it.

HIGH-2 — credentials.py: residual missing per-tenant scoping on get_identity_accounts (service-key trust gap)

File: backend/app/routers/credentials.py (mounted at /api/credentials)

This router was previously hardened: verify_api_key fails closed, the credential encryption key fails closed (503), and the IDOR fix on request_account_credentials / store_account_credentials / GET /account/{account_id}/credentials scopes the lookup by (Account.id == account_id) & (Account.identity_id == identity_id). However the model's own TODO is unresolved: a valid service key is trusted to act on behalf of any identity it names. GET /identity/{identity_id}/accounts does Identity.id == identity_id with no tenant binding — any holder of a service key can enumerate any identity's accounts (account ids, site names, has-stored-credentials flags).

Fix: introduce per-tenant/per-key scoping (map each service key to the identities/tenant it may act for) and verify identity_id is within that key's allowed scope on get_identity_accounts and the credential routes. Tracked here so it is not lost.

MEDIUM-1 — Unauthenticated WebSocket endpoints (documented residuals)

  • sms.py -> WS /api/sms/ws/sms-interceptor
  • infrastructure.py -> WS /api/infrastructure/ws/mobile-commands

Both join a broadcast group with no auth handshake. The code documents the residual (require X-Device-Key as the first frame, verify via _verify_device before joining). OTP values are intentionally not broadcast, which limits impact, but request notifications/commands still flow to any connected socket.

Fix: implement the documented first-frame device-key handshake before connect().

MEDIUM-2 — Weak default secrets / config (defense-in-depth)

  • auth.py: SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key") — the JWT signing key has an insecure default; with the default, tokens are forgeable, which undermines every get_current_user check. Fail closed (no default) outside dev.
  • main.py: CORS/TrustedHost are dev-only (localhost); ensure prod config is environment-driven before exposure.

(Hand secret-hygiene specifics to security if preferred; the authZ-relevant part is the forgeable-JWT risk that underpins all object-level checks.)


Routers reviewed and judged OK (gold-standard pattern in this repo)

  • accounts.py — every route Depends(get_current_user) plus scoped by Identity.user_id == current_user.id; create-path verifies identity ownership; stage update joins through to the owner. OK
  • identities.py — GET/PUT/DELETE by id all scoped (Identity.id == id) & (Identity.user_id == current_user.id); PII encrypted at rest; pydantic models. OK
  • automation.py — JWT-gated. OK
  • chat.py — JWT-gated; /signup scopes identity by owner. (Latent non-authz bug: select is used but not imported — out of appsec scope; flag to backend-engineer.) OK for authz
  • credentials.py — partially hardened; see HIGH-2 for the residual.
  • sms.py, infrastructure.py — device-key plus JWT split correctly applied; see MEDIUM-1 for the WS residual.

Not mounted (not live endpoints — do NOT wire without auth)

background.py, sites.py, sites_mock.py are present but not include_router-ed in main.py. The inline mock sites router in main.py (/api/v1/sites/*) serves static demo data (no DB, no PII) — acceptable as-is but should not be promoted to real data without auth.


Acceptance criteria for the fix (owner: backend-engineer)

  1. google_integration.py: every route gated by get_current_user and every identity/account lookup scoped to the caller; non-owner gets 404. Test added.
  2. llm_scraper.py: all mutating and read routes require auth; unauthenticated -> 401. DELETE additionally restricted. Test added.
  3. site_integrations.py: signup/signin/apikey require auth. Test added.
  4. credentials.py: get_identity_accounts (and the credential routes) enforce per-key/tenant scope. Test added.
  5. WS endpoints: device-key first-frame handshake. Test added.
  6. auth.py: no insecure SECRET_KEY default outside dev.
  7. (Recommend) add governance/architecture-guidelines.md declaring the authZ contract so gate-authz has a baseline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity finding

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions