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 -l → 158 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)
- Verified auth dependency on every non-health route (401 when missing).
- Object-level authz (
permit.check / ownership predicate) on every resource-by-id read & mutation (non-owner → 403).
- CRITICAL secret/exec endpoints (
/sandboxes/{id}/execute, provider credentials, migrations) gated to authorized principals.
- Raw
dict bodies replaced with extra="forbid" pydantic models.
- Regression tests covering 401 (unauth) and 403 (non-owner) for the representative endpoints above.
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-authorizationskill / FuzeFrontbackend/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
/sandboxes/{id}/execute,/organizations/{org}/providers/{p}/credentials/migrations/apply,/migrations/rollback/{v}/{org_id},/{team_id},/{agent_id},/{task_id}reads & mutationsdictrequest bodiesmain.pydictbodies;allow_origins=["*"]+ credentialsCRITICAL-1 — No authentication on ANY orchestrator route (autonomous execution + Docker control exposed)
services/orchestrator/main.pyapp = FastAPI(...)is created with no globaldependencies=[...], and no route usesDepends(get_current_user)—grep -E "Depends\(" main.pyreturns nothing. 158 routes total (@app.get|post|put|delete|patch|websocket), all public-by-default.0.0.0.0:8000and is published (docker-compose.yml:"8000:8000",uvicorn simple_main:app --host 0.0.0.0), withDOCKER_HOST: unix:///var/run/docker.sockconfigured — 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 shellcommandin a sandbox, body is a rawdict, 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}/executeand/tasks/{id}/executereturns 401.CRITICAL-2 — Unauthenticated provider-credential (secret) write, scoped by org id (BOLA on secrets)
main.py:1859POST /organizations/{organization_id}/providers/{provider}/credentialsorganization_idfrom 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.POST /agents/{agent_id}/model-configuration(:1938) andGET .../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}')(orrequireOwnershipon 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.pyPOST /migrations/apply(:653) andPOST /migrations/rollback/{target_version}(:669) construct aMigrationManager(DATABASE_URL)and runmigrate_up/migrate_downwith 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— noowner_id/membership predicate. Same shape for/teams/{team_id}(:259). CORS here isallow_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 importsDependsbut 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
dictrequest bodiesmain.pyhandlers accept un-modelleddictbodies 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 bodystore_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
dict(above) → no schema validation; reject unknown fields with pydantic models.hierarchy_endpoints.py(root) usesCORSMiddleware(allow_origins=["*"]);main.pyusesallow_credentials=Truewith a localhost allow-list — once auth is added, ensureallow_originsis an explicit non-wildcard list wherever credentials are allowed.Out-of-scope note (MCP server)
mcp-servers/fuzeagent-server/server.pydefaults tostdiotransport (local) but also supports anssetransport 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 -l→ 158 routes.grep -E "Depends\(" main.pyfor auth → no auth dependency found./sandboxes/{id}/execute,/tasks/{id}/execute,/organizations/{org}/providers/{p}/credentials,/migrations/apply,GET /organizations/{id}— confirmed no authN and no object-level authz.0.0.0.0:8000+ published port +DOCKER_HOSTsocket viadocker-compose.ymlandentrypoint.sh.Acceptance criteria for closure (backend-engineer)
permit.check/ ownership predicate) on every resource-by-id read & mutation (non-owner → 403)./sandboxes/{id}/execute, provider credentials, migrations) gated to authorized principals.dictbodies replaced withextra="forbid"pydantic models.