Severity: High (defense-in-depth — public exposure already closed separately)
toolserver has no authentication anywhere in its own code. Confirmed by
grepping the whole repo for iam|jwt|auth|bearer|api.key (zero hits outside
test fixtures and an unrelated DAVID-tool SOAP "authenticate" string) and the
full git log (no auth-related commits ever). The IAM_URL: http://auth-service:8001
env var set in omnibioai-studio/docker-compose.yml is dead config — nothing
in this repo reads it.
What's exploitable without it
POST /register_tools accepts a tool definition with a raw http.url,
http.method, arbitrary headers, and body_map — no domain/IP
allowlist. Anyone who can reach this service can register a "tool"
pointing at any address.
POST /runs executes it for real: executor.submit() makes the actual
outbound httpx request from inside the docker network (http_tool_executor.py),
from a position alongside docker-socket-proxy, auth-service, mysql,
neo4j, etc.
GET /runs/{id}/results reads the raw response back.
Chained: an unauthenticated SSRF-as-a-service with response exfiltration,
callable by anything that can reach toolserver:9090.
Status — public exposure already closed (2026-09-01)
The internet-facing path is fixed as of tonight, tracked separately, not what
this issue is about:
tools.omnibioai.org removed from /etc/cloudflared/config.yml (no
longer tunneled to toolserver:9090 directly).
nginx-router.conf's /_svc/toolserver location gated with the same
auth_request+JWT pattern /billing and /_svc/control already use —
deployed and verified (401 on every unauthenticated request, including
POST /register_tools).
Full writeup: see the session report (toolserver-auth-gap-report-2026-09-01.md)
if you have access to it, or the PR/commit history around that date in
omnibioai-studio.
What's still open — this issue
Neither fix above touches same-network callers: anything else on the
docker bridge network (a compromised or misconfigured sibling container, a
future service added without thinking about this) can still hit
toolserver:9090 directly with zero auth. The docker bridge network isn't
internet-reachable on its own, so this isn't urgent the way the public
exposure was — but it's real defense-in-depth for a deployment already
running with a fair number of exposed ports and several services still on
default/shared secrets (JWT_SECRET/AUTH_SECRET_KEY defaulting to
change-me, MYSQL_ROOT_PASSWORD/NEO4J_PASSWORD defaulting to omnibioai,
etc., per omnibioai-studio/docker-compose.yml) — one weak link elsewhere on
that network is enough to reach this if it stays unauthenticated.
Proposed fix
Add a require_permission("workflow.execute") FastAPI dependency to every
route in toolserver_app.py (at minimum /register_tools, /runs,
/validate — arguably /runs/{run_id}* and /capabilities too), doing a
local JWT decode against the shared AUTH_SECRET_KEY/JWT_SECRET. This
matches the pattern already established by every sibling service:
omnibioai-rag/ragbio/api/iam.py — require_permission/_verify
omnibioai-control-center/backend/src/control_center/core/auth.py —
require_permission
omnibioai-dev-hub's api/auth.py — same shared-secret JWT check
workflow.execute is already the permission api-gateway's own
SERVICE_PERMISSION_MAP expects for toolserver (app/core/router.py), so
this closes the loop rather than inventing a new permission name — same
"gateway-level coarse gate + service-level independent, finer check" pattern
omnibioai-rag already uses.
Needs: code change + rebuild/redeploy of the toolserver image. No urgency to
rush ahead of other work, but shouldn't get lost either.
Severity: High (defense-in-depth — public exposure already closed separately)
toolserver has no authentication anywhere in its own code. Confirmed by
grepping the whole repo for
iam|jwt|auth|bearer|api.key(zero hits outsidetest fixtures and an unrelated DAVID-tool SOAP "authenticate" string) and the
full git log (no auth-related commits ever). The
IAM_URL: http://auth-service:8001env var set in
omnibioai-studio/docker-compose.ymlis dead config — nothingin this repo reads it.
What's exploitable without it
POST /register_toolsaccepts a tool definition with a rawhttp.url,http.method, arbitraryheaders, andbody_map— no domain/IPallowlist. Anyone who can reach this service can register a "tool"
pointing at any address.
POST /runsexecutes it for real:executor.submit()makes the actualoutbound
httpxrequest from inside the docker network (http_tool_executor.py),from a position alongside
docker-socket-proxy,auth-service,mysql,neo4j, etc.GET /runs/{id}/resultsreads the raw response back.Chained: an unauthenticated SSRF-as-a-service with response exfiltration,
callable by anything that can reach
toolserver:9090.Status — public exposure already closed (2026-09-01)
The internet-facing path is fixed as of tonight, tracked separately, not what
this issue is about:
tools.omnibioai.orgremoved from/etc/cloudflared/config.yml(nolonger tunneled to
toolserver:9090directly).nginx-router.conf's/_svc/toolserverlocation gated with the sameauth_request+JWT pattern/billingand/_svc/controlalready use —deployed and verified (401 on every unauthenticated request, including
POST /register_tools).Full writeup: see the session report (
toolserver-auth-gap-report-2026-09-01.md)if you have access to it, or the PR/commit history around that date in
omnibioai-studio.What's still open — this issue
Neither fix above touches same-network callers: anything else on the
docker bridge network (a compromised or misconfigured sibling container, a
future service added without thinking about this) can still hit
toolserver:9090directly with zero auth. The docker bridge network isn'tinternet-reachable on its own, so this isn't urgent the way the public
exposure was — but it's real defense-in-depth for a deployment already
running with a fair number of exposed ports and several services still on
default/shared secrets (
JWT_SECRET/AUTH_SECRET_KEYdefaulting tochange-me,MYSQL_ROOT_PASSWORD/NEO4J_PASSWORDdefaulting toomnibioai,etc., per
omnibioai-studio/docker-compose.yml) — one weak link elsewhere onthat network is enough to reach this if it stays unauthenticated.
Proposed fix
Add a
require_permission("workflow.execute")FastAPI dependency to everyroute in
toolserver_app.py(at minimum/register_tools,/runs,/validate— arguably/runs/{run_id}*and/capabilitiestoo), doing alocal JWT decode against the shared
AUTH_SECRET_KEY/JWT_SECRET. Thismatches the pattern already established by every sibling service:
omnibioai-rag/ragbio/api/iam.py—require_permission/_verifyomnibioai-control-center/backend/src/control_center/core/auth.py—require_permissionomnibioai-dev-hub'sapi/auth.py— same shared-secret JWT checkworkflow.executeis already the permission api-gateway's ownSERVICE_PERMISSION_MAPexpects for toolserver (app/core/router.py), sothis closes the loop rather than inventing a new permission name — same
"gateway-level coarse gate + service-level independent, finer check" pattern
omnibioai-ragalready uses.Needs: code change + rebuild/redeploy of the toolserver image. No urgency to
rush ahead of other work, but shouldn't get lost either.