# CVE-2026-27960 — OpenCTI Unauthenticated Authentication Bypass via User Impersonation

## Summary

OpenCTI versions 6.6.0 through 6.9.12 contain an improper-authentication flaw (CWE-287) in the
GraphQL API bearer-token resolution path. The function `authenticateUserByTokenOrUserId()` in
`opencti-platform/opencti-graphql/src/domain/user.js` resolves the HTTP
`Authorization: Bearer <value>` credential against the platform user cache map, which is keyed not
only by each user's secret `api_token`, but also by every non-secret identifier of the user:
`internal_id`, `standard_id`, and STIX ids (`buildStoreEntityMap()` in
`opencti-platform/opencti-graphql/src/database/cache.ts` explicitly pushes `entity.api_token`
into the same id list as `internal_id`/`standard_id`). As a result, an unauthenticated remote
attacker can present **any known or guessable user identifier** — in particular the hard-coded
default-admin `internal_id` `OPENCTI_ADMIN_UUID = 88ec0c6a-13ce-5e39-b486-354fe4a7084f`
(`opencti-platform/opencti-graphql/src/schema/general.js`) — as the bearer token and is
authenticated as that user without ever proving knowledge of the secret API token, password, or
any credential.

## Impact

- Package/component affected: `opencti/platform` (OpenCTI GraphQL API, `opencti-graphql`), all
  deployment modes that expose the HTTP/GraphQL endpoint.
- Affected versions: >= 6.6.0, < 6.9.13 (fixed in 6.9.13).
- Risk level: critical (CVSS 9.8 per public advisories). An unauthenticated network attacker can
  query and mutate the GraphQL API as any existing user, including the default admin: full read
  access to threat-intelligence data and full administrative control (user management, settings,
  data destruction).

## Impact Parity

- Disclosed/claimed maximum impact: unauthenticated remote authentication bypass / authorization
  bypass allowing API access as any existing user, including the default admin (impact class
  `authz_bypass`).
- Reproduced impact from this run: unauthenticated GraphQL request carrying only the public,
  hard-coded default-admin `internal_id` as bearer token was accepted by OpenCTI 6.9.12 and
  executed both `me` (returning the admin identity) and the admin-only `users` listing query.
  The identical request was rejected on the fixed 6.9.13 build, while the real secret
  `api_token` remained accepted on both builds.
- Parity: `full` (unauthenticated admin impersonation through the production GraphQL boundary
  demonstrated end-to-end).

## Root Cause

`authenticateUserFromRequest()` extracts the bearer value and calls
`authenticateUserByTokenOrUserId(context, req, tokenUUID)`. That function only tests
`platformUsers.has(tokenOrId)` on the user cache map. `getEntitiesMapFromCache()` builds this map
via `buildStoreEntityMap()`, which indexes each user under `internal_id`, `standard_id`,
`x_opencti_stix_ids` **and** `api_token`. The code therefore conflates *public identifiers* with
*secret credentials*: possession of a user's internal UUID (for the default admin a constant
compiled into the shipped source, `OPENCTI_ADMIN_UUID`) is treated as proof of identity.

Fix (6.9.13, diff of `src/domain/user.js` between tags 6.9.12 and 6.9.13): the function was split
into `authenticateUserByToken()` — which additionally verifies
`crypto.timingSafeEqual(Buffer.from(user.api_token), Buffer.from(token))` — and
`authenticateUserByUserId()`, which is only reachable after a successfully authenticated
header-provider login (`HEADERS_AUTHENTICATORS`), restoring the invariant that a bearer value must
be the secret token.

- Vendor advisory: https://github.com/OpenCTI-Platform/opencti/security/advisories/GHSA-6vvv-vmfr-xhrx
- Fix: `opencti-platform/opencti-graphql/src/domain/user.js` changes between tags 6.9.12 and 6.9.13.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` (self-contained; only needs Docker and network access
   to pull images).
2. The script:
   - starts the real dependency stack (Elasticsearch 8.19.16, Redis 7, RabbitMQ 3.13, MinIO) on an
     isolated Docker network;
   - starts `opencti/platform:6.9.12` with a configured admin email/password/token, waits for the
     platform health endpoint (migrations included);
   - **attack**: POSTs `{"query":"{ me { id name user_email } }"}` to `/graphql` with
     `Authorization: Bearer 88ec0c6a-13ce-5e39-b486-354fe4a7084f` (the hard-coded default-admin
     `internal_id`, no credentials);
   - **attack 2**: POSTs the admin-only `users(first: 5)` listing with the same header;
   - **controls**: no `Authorization` header, a random unknown UUID bearer, and the real secret
     `api_token` bearer;
   - tears the stack down and repeats attack + valid-token control against
     `opencti/platform:6.9.13` (fixed);
   - writes `bundle/repro/runtime_manifest.json` and exits 0 only if the vulnerable build
     impersonates the admin **and** the fixed build rejects the same request.
3. Expected evidence: on 6.9.12 the attack response contains
   `"user_email":"admin@opencti.io"` for both `me` and `users` queries; controls without a valid
   secret token return no identity; on 6.9.13 the attack returns no identity while the real token
   still authenticates.

## Evidence

- Driver log: `bundle/logs/reproduction_steps.log`
- Attack responses: `bundle/artifacts/opencti/vuln_attack_me_response.json`,
  `bundle/artifacts/opencti/vuln_attack_users_response.json`
- Controls: `bundle/artifacts/opencti/vuln_control_noauth_response.json`,
  `bundle/artifacts/opencti/vuln_control_random_uuid_response.json`,
  `bundle/artifacts/opencti/vuln_control_valid_token_response.json`
- Fixed-version negative control: `bundle/artifacts/opencti/fixed_attack_me_response.json`,
  `bundle/artifacts/opencti/fixed_control_valid_token_response.json`
- Platform logs: `bundle/artifacts/opencti/platform_vuln.log`,
  `bundle/artifacts/opencti/platform_fixed.log`
- Runtime manifest: `bundle/repro/runtime_manifest.json`
- Environment: Docker 29, `opencti/platform:6.9.12` vs `opencti/platform:6.9.13`,
  Elasticsearch 8.19.16, Redis 7-alpine, RabbitMQ 3.13-management-alpine, MinIO latest.

Key excerpts are recorded in `bundle/logs/reproduction_steps.log` (vulnerable build returns the
admin identity for the hard-coded UUID bearer; fixed build rejects it).

## Recommendations / Next Steps

- Upgrade to OpenCTI >= 6.9.13.
- Interim (partial) workaround per vendor: set `APP__ADMIN__EXTERNALLY_MANAGED` to disable the
  default admin account — note this does not close the bypass for other users, since any user
  `internal_id`/`standard_id` remains a valid bearer on vulnerable builds.
- Treat all user `internal_id`/`standard_id` values as public; rotate admin API tokens if a
  vulnerable version was exposed.
- Regression test: assert that `Authorization: Bearer <user internal_id>` is rejected by the
  GraphQL endpoint while the user's `api_token` is accepted.

## Additional Notes

- The script is idempotent: it recreates the Docker network/containers on each run and cleans them
  up on exit (trap). It was executed twice consecutively with identical pass results.
- No sanitizer, mock, or reimplementation is used: the proof exercises the shipped
  `opencti/platform` container through its real HTTP/GraphQL listener.
- The exploit requires no information beyond what is compiled into the public source tree
  (`OPENCTI_ADMIN_UUID`), so default deployments are exploitable with zero reconnaissance;
  impersonating *other* users additionally requires their `internal_id`/`standard_id`, which are
  routinely exposed in API responses to authenticated parties.
