Skip to content

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

Description

@fuzeone-bot

appsec: BOLA / authorization audit — FuzeAgent FastAPI orchestrator

Reviewer: appsec-reviewer (independent endpoint-authorization review)
Date: 2026-06-28
Scope reviewed: services/orchestrator/main.py (158 routes), services/orchestrator/main_with_hierarchy.py, services/orchestrator/coordination_endpoints.py, services/orchestrator/hierarchy_endpoints.py, hierarchy_endpoints.py (root), quick_hierarchy_api.py, simple_proxy.py, mcp-servers/fuzeagent-server/server.py.
Reference standard: endpoint-authorization skill / FuzeFront backend/src/middleware/permissions.ts (requirePermission, requireOwnership, permit.check).
Owner for the fix: backend-engineer (this issue is a finding; do NOT implement here).


Summary by severity

Sev Class Representative
CRITICAL Missing authN on autonomous-execution / control endpoints entire orchestrator (158 routes, 0 auth)
CRITICAL Unauthenticated arbitrary command execution + secret write /sandboxes/{id}/execute, /organizations/{org}/providers/{p}/credentials
HIGH Missing authN on DB-migration control /migrations/apply, /migrations/rollback/{v}
HIGH Object-level authz (BOLA/IDOR) — resource-by-id, no ownership/permission gate all /{org_id}, /{team_id}, /{agent_id}, /{task_id} reads & mutations
MEDIUM Mass-assignment (BOPLA) — raw dict request bodies 8+ handlers in main.py
MEDIUM Input validation gaps / permissive CORS raw dict bodies; allow_origins=["*"] + credentials

CRITICAL-1 — No authentication on ANY orchestrator route (autonomous execution + Docker control exposed)

services/orchestrator/main.py

  • app = FastAPI(...) is created with no global dependencies=[...], and no route uses Depends(get_current_user)grep -E "Depends\(" main.py returns nothing. 158 routes total (@app.get|post|put|delete|patch|websocket), all public-by-default.
  • The service binds 0.0.0.0:8000 and is published (docker-compose.yml: "8000:8000", uvicorn simple_main:app --host 0.0.0.0), with DOCKER_HOST: unix:///var/run/docker.sock configured — i.e. the unauthenticated API fronts container/agent execution.

Because the orchestrator performs autonomous task execution and container control, every unauthenticated control endpoint is CRITICAL. Highest-impact examples:

  • POST /tasks/{task_id}/execute (main.py:897) — "Begin autonomous execution of a task using Claude SDK integration." No auth.
  • POST /sandboxes/{sandbox_id}/execute (main.py:1194) — executes an arbitrary shell command in a sandbox, body is a raw dict, no auth, no validation. Unauthenticated RCE-by-design.
  • DELETE /sandboxes/{sandbox_id} (main.py:1215), POST /agents (:679), POST /agents/{agent_id}/tasks (:712), POST /tasks/{task_id}/cancel (:1115), POST /tasks/{task_id}/claude-session/input (:1482), POST /agents/{from}/communicate/{to} (:1744).

Fix (backend-engineer): put every route behind a verified auth dependency — apply it app-wide via FastAPI(dependencies=[Depends(get_current_user)]) (or per-router), with a health/readiness allow-list. Then layer object-level authz (below). Add a regression test that an unauthenticated request to /sandboxes/{id}/execute and /tasks/{id}/execute returns 401.


CRITICAL-2 — Unauthenticated provider-credential (secret) write, scoped by org id (BOLA on secrets)

main.py:1859 POST /organizations/{organization_id}/providers/{provider}/credentials

  • "Store encrypted API credentials for a model provider at organization level." Takes organization_id from the path and writes provider API keys. No auth, no check that the caller belongs to / owns that organization. Any caller can write (or overwrite) provider secrets into ANY organization by guessing/enumerating its id — and the paired config endpoints let an attacker repoint agents at attacker-controlled models/keys.
  • Related: POST /agents/{agent_id}/model-configuration (:1938) and GET .../model-configuration (:1980) — set/read agent model config for any agent id, no ownership gate.

Fix: require auth + permit.check(user, 'write', f'organization:{organization_id}') (or requireOwnership on the org) before storing credentials; never trust the path id as the authorization boundary.


HIGH-1 — Unauthenticated database-migration control

services/orchestrator/main_with_hierarchy.py

  • POST /migrations/apply (:653) and POST /migrations/rollback/{target_version} (:669) construct a MigrationManager(DATABASE_URL) and run migrate_up / migrate_down with no auth. An unauthenticated caller can advance or roll back the schema (data-destructive).

Fix: restrict to an authenticated admin/service principal (auth dependency + role/permission check); these should not be reachable on the public API surface at all.


HIGH-2 — Object-level authorization missing (BOLA / IDOR) on every resource-by-id

The codebase consistently loads/mutates resources by a path id with no ownership filter and no permit.check — the canonical BOLA pattern.

Representative handlers:

  • hierarchy_endpoints.py (root) GET /organizations/{organization_id} (:159): SELECT ... FROM organizations WHERE id = $1 — no owner_id/membership predicate. Same shape for /teams/{team_id} (:259). CORS here is allow_origins=["*"] (:28).
  • main_with_hierarchy.py: GET/PUT/DELETE /organizations/{org_id} (:240 / :253 / :274), GET/PUT/DELETE /teams/{team_id} (:347 / :360 / :380), GET /agents/{agent_id} (:483) — all fetch/update/delete by id with only a 404-if-missing check, never an authorization check.
  • main.py: GET /tasks/{task_id} (:838), PUT /tasks/{task_id} (:939), GET /agents/{agent_id}/status|tasks|memory|conversations|performance|sandbox, GET /tasks/{task_id}/messages|conversation|file-operations, POST /tasks/{task_id}/file-operations/{batch_id}/approve|rollback — by-id access with no ownership/permission gate. Approving/rolling back another tenant's file-operation batch is a direct cross-tenant integrity violation.
  • coordination_endpoints.py: GET /coordination/products/{product_id} (:110), GET/PUT /coordination/requests/{request_id} (:261 / :293) — by-id, no authz (note: this file imports Depends but never uses it for auth).

Fix: after authN, authorize the specific object: permit.check(user, action, f'{resource}:{id}', ctx) and/or an ownership predicate in the query (WHERE id = $1 AND owner_id = <current_user> / membership join). Add tests: non-owner → 403, owner → 200.


MEDIUM-1 — Mass-assignment (BOPLA) via raw dict request bodies

main.py handlers accept un-modelled dict bodies and forward fields straight through (no pydantic allow-list):

  • create_agent_from_template(request: dict) (:845)
  • update_task(task_id, update_data: dict) (:939) — sets status/result from raw body
  • store_interaction(interaction_data: dict) (:950)
  • execute_command_in_sandbox(sandbox_id, command_data: dict) (:1194)
  • register_agent(agent_id, registration_data: dict) (:1227)
  • report_agent_error(agent_id, error_data: dict) (:1262)
  • send_claude_session_input(task_id, input_data: dict) (:1483)
  • send_agent_communication(from, to, communication_data: dict) (:1745)

Fix: replace each with an explicit pydantic model (extra = "forbid"), accepting only the intended fields; never let derived/sensitive fields (status, owner, ids) be client-settable.


MEDIUM-2 — Input validation / CORS hardening

  • Several bodies are raw dict (above) → no schema validation; reject unknown fields with pydantic models.
  • hierarchy_endpoints.py (root) uses CORSMiddleware(allow_origins=["*"]); main.py uses allow_credentials=True with a localhost allow-list — once auth is added, ensure allow_origins is an explicit non-wildcard list wherever credentials are allowed.

Out-of-scope note (MCP server)

mcp-servers/fuzeagent-server/server.py defaults to stdio transport (local) but also supports an sse transport with no auth (run_sse(host, port)). If/when the SSE transport is exposed over the network, it inherits the same unauthenticated-control risk — track separately when that channel is enabled.


Verification performed

  • grep -E "@app\.(get|post|put|delete|patch|websocket)" main.py | wc -l158 routes.
  • grep -E "Depends\(" main.py for auth → no auth dependency found.
  • Read the handler bodies for /sandboxes/{id}/execute, /tasks/{id}/execute, /organizations/{org}/providers/{p}/credentials, /migrations/apply, GET /organizations/{id} — confirmed no authN and no object-level authz.
  • Confirmed bind 0.0.0.0:8000 + published port + DOCKER_HOST socket via docker-compose.yml and entrypoint.sh.

Acceptance criteria for closure (backend-engineer)

  1. Verified auth dependency on every non-health route (401 when missing).
  2. Object-level authz (permit.check / ownership predicate) on every resource-by-id read & mutation (non-owner → 403).
  3. CRITICAL secret/exec endpoints (/sandboxes/{id}/execute, provider credentials, migrations) gated to authorized principals.
  4. Raw dict bodies replaced with extra="forbid" pydantic models.
  5. Regression tests covering 401 (unauth) and 403 (non-owner) for the representative endpoints above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity findings / vulnerabilities

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions