Audit finding: BUG-R3-S2-A8-H1 | CWE-284 | CVSS 7.1 (PR:L, S:C, C:H) | PoC: pending (two-request chain, verified in code)
Summary
require_metrics_access (api/main.py:204-222) admits anyone passing _has_metrics_admin_role (api/main.py:199-201):
return user is not None and user.is_active and user.role in {"owner", "admin"}
That is a per-organization role check with no organization_id comparison, and there is no platform-operator concept anywhere in the data model. Because open self-service POST /auth/signup hardcodes role="owner" for every new account (auth/routes.py:62) — and it is the only user-creation path — the gate degenerates to "possess any account", which is one anonymous request away.
GET /metrics then returns metrics_collector.get_all_tenant_metrics() (api/main.py:1068): for every organization — request volumes, success/failure/blocked counts, success rates, latencies, LLM provider usage, last-activity timestamps; GET /metrics/prometheus exposes the per-tenant labeled series. Sequential org PKs give complete tenant enumeration. The API-key branch is equally open, and ApiKey has no expiry field, so the cross-tenant read persists indefinitely.
This contradicts the codebase's own org-scoped sibling (/metrics/{organization_id} returns 403 for other orgs) and the audit routes' org filters.
Attack
POST /auth/signup (anonymous) -> GET /metrics with the returned JWT. Persistence variant: mint an API key. Works in every deployment shape that serves the API.
Suggested fix
Treat all-tenant metrics as a platform-operator capability: explicit env-configured allowlist (e.g. QWED_METRICS_OPERATOR_USER_IDS), fail-closed when unset. Never treat an org role as platform-wide authority. See audit patch sketch.
Related: #222 (RBAC middleware non-functional — same authorization-scope confusion class).
Source: OpenVuln external audit of QWED-AI/qwed-verification (snapshot v7.0.0, re-verified against current main v7.1.0). All code anchors below were spot-checked against current main before filing.
Audit finding:
BUG-R3-S2-A8-H1| CWE-284 | CVSS 7.1 (PR:L,S:C,C:H) | PoC: pending (two-request chain, verified in code)Summary
require_metrics_access(api/main.py:204-222) admits anyone passing_has_metrics_admin_role(api/main.py:199-201):That is a per-organization role check with no
organization_idcomparison, and there is no platform-operator concept anywhere in the data model. Because open self-servicePOST /auth/signuphardcodesrole="owner"for every new account (auth/routes.py:62) — and it is the only user-creation path — the gate degenerates to "possess any account", which is one anonymous request away.GET /metricsthen returnsmetrics_collector.get_all_tenant_metrics()(api/main.py:1068): for every organization — request volumes, success/failure/blocked counts, success rates, latencies, LLM provider usage, last-activity timestamps;GET /metrics/prometheusexposes the per-tenant labeled series. Sequential org PKs give complete tenant enumeration. The API-key branch is equally open, andApiKeyhas no expiry field, so the cross-tenant read persists indefinitely.This contradicts the codebase's own org-scoped sibling (
/metrics/{organization_id}returns 403 for other orgs) and the audit routes' org filters.Attack
POST /auth/signup(anonymous) ->GET /metricswith the returned JWT. Persistence variant: mint an API key. Works in every deployment shape that serves the API.Suggested fix
Treat all-tenant metrics as a platform-operator capability: explicit env-configured allowlist (e.g.
QWED_METRICS_OPERATOR_USER_IDS), fail-closed when unset. Never treat an org role as platform-wide authority. See audit patch sketch.Related: #222 (RBAC middleware non-functional — same authorization-scope confusion class).
Source: OpenVuln external audit of QWED-AI/qwed-verification (snapshot v7.0.0, re-verified against current
mainv7.1.0). All code anchors below were spot-checked against current main before filing.