Missing Authentication for Critical Function and Initialization of a Resource with an Insecure Default in aisoc
What fixes it
- 12.0.0aisoc, otherGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
services/actions executes response actions against customer estates: isolate a host, disable an account, block an IP, kill a process, run a script. Every mutating route is meant to require a shared service bearer token.
In the shipped, unmodified deployment that token is never generated and the dev-mode escape is on by default, so the authentication guard returns early and no authentication is performed at all. POST /api/v1/live-actions/dispatch — the endpoint that isolates hosts, disables accounts and blocks IPs — executes for an anonymous caller who sends no Authorization header.
The root cause is a mismatch between two halves of this repository: scripts/ensure_env.py generates three secrets and not this one, while docker-compose.yml defaults AISOC_DEV_MODE to 1. The same image with a token configured returns 401 correctly, which isolates the defect to configuration as shipped.
**Severity: Critical — CVSS v3.1 AV:A/AC:L/PR:N/UI:N/S:C/C:L/I:H/A:H = 9.5**
Weaknesses: CWE-306 (Missing Authentication for Critical Function), CWE-1188 (Insecure Default Initialization of Resource)
Verified by execution, with a control (PoC §5).
---
Details
Component: services/actions + docker-compose.yml + scripts/ensure_env.py. Image ghcr.io/beenuar/aisoc-actions:latest.
Attack surface
| Route | Code | Guard | |---|---|---| | POST /api/v1/live-actions/dispatch | services/actions/app/api/live_actions_router.py:160 | Router-level dependencies=[Depends(require_service_auth)] at :59 | | GET /api/v1/live-actions | live_actions_router.py:87 | same | | POST /api/v1/actions | services/actions/app/api/router.py:53 | Depends(require_service_auth) | | POST /api/v1/actions/{id}/approve | app/api/router.py:178 | same | | POST /api/v1/actions/{id}/reject | app/api/router.py:233 | same | | GET /api/v1/actions/{id} | app/api/router.py:265 | same |
Port: docker-compose.yml maps "${AISOC_BIND_ADDR:-127.0.0.1}:8002:8085"; the container listens on 0.0.0.0:8085 (services/actions/Dockerfile). Because the container binds all interfaces on the compose network, the service is reachable from any container on that network even when the host port is published to loopback only.
Defect 1 — the guard fails open when the token is empty and dev-mode is on
# services/actions/app/security/authz.py:104-122
async def require_service_auth(authorization: str | None = Header(default=None)) -> None: # :104
"""FastAPI dependency: verify the service bearer token on mutating routes
(W4.3). Fails closed in production when no token is configured."""
settings = get_settings()
token = settings.AISOC_ACTIONS_SERVICE_TOKEN.strip() # :108
if not token:
if settings.AISOC_DEV_MODE: # :110
return # :111 <-- no authentication performed
raise HTTPException(status_code=503, detail="actions service auth is not configured")
expected = f"Bearer {token}"
if not authorization or not hmac.compare_digest(authorization, expected):
raise HTTPException(status_code=401, detail="invalid or missing service token")The early return at :111 is correct only if a deployment can never reach it with dev-mode on. It can.
Defect 2 — the token is never generated
# scripts/ensure_env.py:82-95
GENERATED: dict[str, tuple[Callable[[], str], str]] = { # :82
"AISOC_CREDENTIAL_KEY": (...), # :83
"SECRET_KEY": (...), # :87
"AISOC_SERVICE_TOKEN": (...), # :91
}AISOC_ACTIONS_SERVICE_TOKEN is absent. scripts/ensure_env.py is the script make up runs to create .env and fill its generated secrets, so a stock install leaves it unset. It is also **not documented in .env.example** — searching that file for the variable returns nothing.
Defect 3 — dev-mode defaults on
# docker-compose.yml (actions service)
# Must match AISOC_ACTIONS_SERVICE_TOKEN on the api and slack-bot
# services. Unset in dev, where AISOC_DEV_MODE relaxes the check;
# require_service_auth returns 503 on every mutating route without it
# in any other environment, which is the intended failure.
AISOC_ACTIONS_SERVICE_TOKEN: ${AISOC_ACTIONS_SERVICE_TOKEN:-} # :862 empty unless the operator sets it
AISOC_DEV_MODE: ${AISOC_DEV_MODE:-1} # :863 defaults ONThe comment at :858-861 states the intended contract: *"require_service_auth returns 503 on every mutating route without it in any other environment, which is the intended failure."*
That contract assumes "any other environment" occurs. It does not: AISOC_DEV_MODE defaults to 1 in the same file, so out-of-the-box the service is permanently "dev" for authentication purposes and the 503 branch is unreachable.
Defect 4 — the authorization gate beneath it is also bypassable
authorize_actionreturns early whenrequest.principal is NoneandAISOC_ACTIONS_REQUIRE_PRINCIPALisFalse— its default (services/actions/app/core/config.py:93-96).POST /actions/{id}/approveaccepts a caller-suppliedActionPrincipal, including itspermissionslist (services/actions/app/api/router.py:178-217), so a caller able to reach the port can also satisfy the approval requirement.
Note on scope: the shipped default for AISOC_ACTIONS_REQUIRE_APPROVER is True (services/actions/app/core/config.py:103-106), so *if* the token is configured, the approve path correctly refuses an identity-less call. That default limits the blast radius in a correctly-configured deployment — but it is not in play when the service is unauthenticated, which is the state this report is about.
Why the existing controls do not stop it
require_service_authitself is correct (hmac.compare_digest, fail-closed 503) — but its empty-token branch is gated on a flag that defaults to on..env.exampledoes not document the variable, so an operator reading the documented configuration has no instruction to set it.ensure_env.pygeneratesAISOC_SERVICE_TOKEN(used by connectors, ueba, honeytokens, purple-team, slack-bot) but not the actions-service token, even thoughdocker-compose.yml:858says they "must match".- Nothing in
infra/(Helm, Terraform) sets it either.
---
PoC
1. Run the shipped configuration
docker compose build actions docker network create aisoc-test docker run -d --name aisoc-actions-test --network aisoc-test -p 127.0.0.1:18085:8085 \ -e AISOC_DEV_MODE=1 -e AISOC_ACTIONS_SERVICE_TOKEN= \ ghcr.io/beenuar/aisoc-actions:latest
Wait for Application startup complete. in docker logs aisoc-actions-test.
2. Anonymous requests — **no Authorization header is sent**
$ curl -i http://127.0.0.1:18085/api/v1/live-actions
HTTP 200
-> 73 executors, 22 vendors enumerated
["aws","azure_entra","cloudflare","crowdstrike","defender","elastic","fortigate",
"google_workspace","jira","okta","pagerduty","panos","qradar","sentinel",
"sentinelone","servicenow","slack","splunk","teams", ...]
$ curl -X POST http://127.0.0.1:18085/api/v1/live-actions/dispatch \
-H 'Content-Type: application/json' \
-d '{"capability":"isolate_host","vendor_id":"crowdstrike",
"target":"VICTIM-HOST-01","params":{},"dry_run":false}'
HTTP 200
{"result":{"request_id":"679916dd-...","status":"simulated",
"capability":"isolate_host","vendor_id":"crowdstrike",
"summary":"Simulated isolate host VICTIM-HOST-01", ...}}The dispatch endpoint executed the action. "simulated" reflects only that no CrowdStrike credentials were supplied in this request — it is an execution outcome, not an authentication refusal.
3. Control — identical image, token configured
docker run -d --name aisoc-actions-ctl --network aisoc-test -p 127.0.0.1:18086:8085 \ -e AISOC_DEV_MODE=1 -e AISOC_ACTIONS_SERVICE_TOKEN='s3cr3t-token-set-by-operator' \ ghcr.io/beenuar/aisoc-actions:latest
$ curl -i http://127.0.0.1:18086/api/v1/live-actions # no header
HTTP 401 {"detail":"invalid or missing service token"}
$ curl -i -X POST http://127.0.0.1:18086/api/v1/live-actions/dispatch # no header
HTTP 401 {"detail":"invalid or missing service token"}
$ curl -i -X POST http://127.0.0.1:18086/api/v1/live-actions/dispatch \
-H 'Authorization: Bearer s3cr3t-token-set-by-operator' ...
HTTP 200The only difference between the two runs is configuration. That is what identifies this as an insecure-default defect rather than an authorization bug in the code path.
---
Impact
- CWE-306 / CWE-1188. Unauthenticated remote dispatch of containment actions to anyone who can reach the port — any process on the host, or any container on the
aisoccompose network, including a compromised lower-value service (web, ingest, a connector). Contained actions includeisolate_host,disable_user,block_ip,kill_processandrun_script. - Chained impact. Combined with the separate command-injection finding in the CrowdStrike RTR client (
crowdstrike_rtr.py:273,endpoint.py:442,crowdstrike_rtr.py:392), this makes those injection sinks reachable without any credential, provided the caller supplies vendor credentials or reaches the playbook path (services/api/app/services/playbook_step_dispatch.py:181) where AiSOC supplies them from the vault. - Reconnaissance.
GET /api/v1/live-actionsenumerates the estate's integrations — which EDR, IdP, cloud and network vendors the customer runs — to an unauthenticated caller. - Who is impacted: every deployment created with the shipped
docker-compose.ymlandmake env, i.e. the documented default install. - Honest limitation. With no vendor credentials available to the attacker, dispatched actions return
simulatedand touch no vendor. The finding is the authentication bypass, not unattended containment; real estate impact requires either the attacker holding vendor credentials or the playbook path, where the platform supplies them.
Suggested remediation: (1) add AISOC_ACTIONS_SERVICE_TOKEN to GENERATED in scripts/ensure_env.py:82 and document it in .env.example; (2) default AISOC_DEV_MODE to 0 in docker-compose.yml:863; (3) make the unconfigured-credential case a startup error rather than a request-time pass, regardless of dev-mode; (4) default AISOC_ACTIONS_REQUIRE_PRINCIPAL to True (services/actions/app/core/config.py:93) and stop accepting caller-supplied permissions on the approve route (services/actions/app/api/router.py:178-217); (5) audit the sibling services — the same _dev_mode() early-return is vendored into connectors, ueba, honeytokens and purple-team (app/security/service_auth.py:66 in each), protected today only because ensure_env.py happens to generate AISOC_SERVICE_TOKEN.
---
Maintainer response
Confirmed and fixed. The dev-mode exemption is gone from require_service_auth(); the actions service now answers 503 on every mutating route when AISOC_ACTIONS_SERVICE_TOKEN is unset rather than serving unauthenticated, and make env generates that token so a first run is not the insecure case.
You were right that this was reachable on a stock install. Thank you for the report.
Fixed in v12.0.0.
Text from GitHub Advisory Database
Affected versions 1
- aisoc, other>= 8.0.0, <= 11.2.0GitHub