Summary
GET /api/ai/usage/summary and GET /api/ai/usage/by-provider (crates/temps-ai-gateway/src/handlers/usage.rs) are gated only by permission_guard!(auth, AiGatewayRead), with no ownership check tying the returned rows to projects/environments the caller actually has access to.
Permission::AiGatewayRead is held by default by Role::User, Role::Reader, Role::Admin, and Role::PlatformAdmin (crates/temps-auth/src/permissions.rs) — i.e. any authenticated project member, not just operators.
Impact
A caller with AiGatewayRead (the default for ordinary project members, granted for their own day-to-day AI usage) can pass an arbitrary project_id or environment_id query parameter and read AI usage data — including deployment_token_id — for any project on the instance, not just their own. This is cross-tenant enumeration of model usage, token volumes, and deployment token IDs belonging to other tenants.
Root cause
The handlers build their UsageFilter directly from query parameters (UsageQueryParams::to_filter()) with no scoping to auth.user_id() or a project-ownership check. usage_service.rs::build_filter_clause does support a user_id filter, but the handlers never populate it from the authenticated caller — filtering is entirely caller-controlled and opt-in.
Discovery context
Found during a /review-pr security-audit pass on PR #543 ("feat(ai-gateway): add project usage governance"), which added the project_id/environment_id query-param wiring that makes this easy to target. The underlying authorization gap (no ownership scoping at all) predates that PR — the endpoint already returned the full instance-wide dataset to any AiGatewayRead holder when unfiltered; the PR only added a convenient way to slice that already-unscoped data by project/environment. Not blocking that PR's merge; filed separately per this repo's existing pattern for the same bug class (see #403, the analogous proxy-logs cross-project IDOR).
Suggested fix
Either:
- Add a
require_operator-style check (only operators may query other projects' usage), or
- When the caller is not an operator, restrict the query to
project_id IN (<projects the caller is authorized for>) server-side, ignoring/rejecting a caller-supplied project_id outside that set.
Summary
GET /api/ai/usage/summaryandGET /api/ai/usage/by-provider(crates/temps-ai-gateway/src/handlers/usage.rs) are gated only bypermission_guard!(auth, AiGatewayRead), with no ownership check tying the returned rows to projects/environments the caller actually has access to.Permission::AiGatewayReadis held by default byRole::User,Role::Reader,Role::Admin, andRole::PlatformAdmin(crates/temps-auth/src/permissions.rs) — i.e. any authenticated project member, not just operators.Impact
A caller with
AiGatewayRead(the default for ordinary project members, granted for their own day-to-day AI usage) can pass an arbitraryproject_idorenvironment_idquery parameter and read AI usage data — includingdeployment_token_id— for any project on the instance, not just their own. This is cross-tenant enumeration of model usage, token volumes, and deployment token IDs belonging to other tenants.Root cause
The handlers build their
UsageFilterdirectly from query parameters (UsageQueryParams::to_filter()) with no scoping toauth.user_id()or a project-ownership check.usage_service.rs::build_filter_clausedoes support auser_idfilter, but the handlers never populate it from the authenticated caller — filtering is entirely caller-controlled and opt-in.Discovery context
Found during a
/review-prsecurity-audit pass on PR #543 ("feat(ai-gateway): add project usage governance"), which added theproject_id/environment_idquery-param wiring that makes this easy to target. The underlying authorization gap (no ownership scoping at all) predates that PR — the endpoint already returned the full instance-wide dataset to anyAiGatewayReadholder when unfiltered; the PR only added a convenient way to slice that already-unscoped data by project/environment. Not blocking that PR's merge; filed separately per this repo's existing pattern for the same bug class (see #403, the analogous proxy-logs cross-project IDOR).Suggested fix
Either:
require_operator-style check (only operators may query other projects' usage), orproject_id IN (<projects the caller is authorized for>)server-side, ignoring/rejecting a caller-suppliedproject_idoutside that set.