Fix multi-tenancy bypass: derive tenant_id from JWT, not from request headers #5

Open
opened 2026-04-20 09:34:28 +00:00 by sharang · 0 comments
Owner

Problem

compliance/api/tenant_utils.py:46-58 resolves tenant_id from X-Tenant-ID header or query param supplied by the client. There is no check that the authenticated user belongs to that tenant.

Any authenticated user can set X-Tenant-ID: <victim_uuid> and read or modify another tenant's compliance data — all VVT entries, DSFA documents, DSR requests, change requests, company profiles.

Required Actions

  1. After #4 (JWT middleware) is merged: extract tenant_id from the validated JWT claims (e.g. azp or a custom claim set by Keycloak)
  2. Remove X-Tenant-ID header as an input to get_tenant_id() — the header is untrusted user input
  3. If the JWT contains no tenant claim, return 403 with a clear error
  4. Add a DB-level assertion helper: assert_tenant_owns(db, tenant_id, table, record_id) used in every GET/PATCH/DELETE handler

Files to Change

  • compliance/api/tenant_utils.py — rewrite get_tenant_id()
  • All route handlers that currently accept x_tenant_id: str = Header(...)

Acceptance Criteria

  • X-Tenant-ID header is ignored by all routes; tenant comes from JWT only
  • Test: authenticated user for tenant A cannot retrieve any records for tenant B (returns 403 or 404, never 200 with data)
  • Depends on: Wire JWT middleware to all FastAPI routes (#4)
## Problem `compliance/api/tenant_utils.py:46-58` resolves tenant_id from `X-Tenant-ID` header or query param supplied by the client. There is no check that the authenticated user belongs to that tenant. Any authenticated user can set `X-Tenant-ID: <victim_uuid>` and read or modify another tenant's compliance data — all VVT entries, DSFA documents, DSR requests, change requests, company profiles. ## Required Actions 1. After #4 (JWT middleware) is merged: extract `tenant_id` from the validated JWT claims (e.g. `azp` or a custom claim set by Keycloak) 2. Remove `X-Tenant-ID` header as an input to `get_tenant_id()` — the header is untrusted user input 3. If the JWT contains no tenant claim, return 403 with a clear error 4. Add a DB-level assertion helper: `assert_tenant_owns(db, tenant_id, table, record_id)` used in every GET/PATCH/DELETE handler ## Files to Change - `compliance/api/tenant_utils.py` — rewrite `get_tenant_id()` - All route handlers that currently accept `x_tenant_id: str = Header(...)` ## Acceptance Criteria - `X-Tenant-ID` header is ignored by all routes; tenant comes from JWT only - Test: authenticated user for tenant A cannot retrieve any records for tenant B (returns 403 or 404, never 200 with data) - Depends on: #4
sharang added this to the M1: Security Foundation milestone 2026-04-20 09:34:28 +00:00
sharang added the severity: highsecurity labels 2026-04-20 09:34:28 +00:00
Sign in to join this conversation.