GHSA-C6W9-5G5J-JH2P
Vulnerability from github – Published: 2026-07-20 21:48 – Updated: 2026-07-20 21:48Summary
When response caching is enabled (CACHE_ENABLED=true), the cache-key derivation in api/src/utils/get-cache-key.ts includes only version, path, query, and accountability.user (plus a conditional ip). Authorization context beyond user (share, role, roles, admin, app, policies) is not part of the key.
For share tokens this is load-bearing. Directus's share-authentication flow (api/src/services/shares.ts:100-105) issues a JWT without an id claim, so api/src/utils/get-accountability-for-token.ts never assigns accountability.user, leaving it null (the default from create-default-accountability.ts). Every share token, and every anonymous request, therefore reduces to user: null in the cache-key input. Two different shares (or an anonymous request and a share token) requesting the same URL with the same query produce identical cache keys. The first request populates the bucket with a permission-filtered response; subsequent hits from unrelated shares or anonymous clients receive that payload without any permission re-evaluation.
This is the web-cache pattern "authorization-dependent response cached under an unsegmented key" (cache key collision / missing authorization context in cache key, CWE-524 and CWE-639). Two adjacent read populations collide:
- Share to share:
Share Apopulates the cache,Share BreadsShare A's scoped response. - Share to anonymous (and the reverse): any unauthenticated client hitting the same URL retrieves cached share-scoped data without presenting any token.
Affected
- Config required:
CACHE_ENABLED=true(any store: memory, redis, memcached) plus at least one activedirectus_sharesrow. This is not a default-on bug:CACHE_ENABLEDships asfalse. The cache is documented as a production performance setting, so operators who enable it are the ones affected.
Vulnerability class
- CWE-524: Use of Cache Containing Sensitive Information
- CWE-639: Authorization Bypass Through User-Controlled Key (the key here is the derived cache key, not the URL)
- OWASP API3:2023: Broken Object Property Level Authorization
Impact
- Cross-share confidentiality breach. Any share's filtered response can be served to a holder of a different share token, or to an anonymous request, that hits the same URL and query. Shares are advertised as a mechanism to distribute scoped, read-only access to specific items; this bug makes every cached share response readable by any other share-token holder who can reach the URL (the item-detail admin UI uses a predictable pattern).
- Anonymous request can read share data. Anonymous requests also compute
user=null. An anonymous client hitting/items/articles?fields=*after a share request has populated the cache receives the share's scoped payload with zero authentication. - Password-protected share, derivative effect. Password protection lives only at
shares.login(JWT issuance). Once any share has populated the cache for a URL, an anonymous or alternate-share request to that URL retrieves the cached payload without exchanging the share password. This is the same cache-key collision surfacing as a password-protection bypass symptom, not a distinct mechanism. - Persistence. The leak persists for the
CACHE_TTLwindow (commonly 5 to 30 minutes).CACHE_AUTO_PURGEclears on mutating writes to cached collections but does not purge per-user or per-share. WithCACHE_STORE=redis(common in production) the poisoned bucket survives server restarts. - No write impact. The bypass is read-only.
Scope of the leak depends on the share's permission surface. A share with no backing role (directus_shares.role = null, the default) collapses visibility to the primary key, so the cache leaks only PKs. A share backed by a role with broader field access (the intended production setup for distributing useful content) leaks the full content that role can see on the scoped item.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "directus"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "12.0.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-61836"
],
"database_specific": {
"cwe_ids": [
"CWE-524",
"CWE-639"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T21:48:15Z",
"nvd_published_at": "2026-07-15T15:16:48Z",
"severity": "HIGH"
},
"details": "## Summary\n\nWhen response caching is enabled (`CACHE_ENABLED=true`), the cache-key derivation in `api/src/utils/get-cache-key.ts` includes only `version`, `path`, `query`, and `accountability.user` (plus a conditional `ip`). Authorization context beyond `user` (`share`, `role`, `roles`, `admin`, `app`, `policies`) is not part of the key.\n\nFor share tokens this is load-bearing. Directus\u0027s share-authentication flow (`api/src/services/shares.ts:100-105`) issues a JWT without an `id` claim, so `api/src/utils/get-accountability-for-token.ts` never assigns `accountability.user`, leaving it `null` (the default from `create-default-accountability.ts`). Every share token, and every anonymous request, therefore reduces to `user: null` in the cache-key input. Two different shares (or an anonymous request and a share token) requesting the same URL with the same query produce identical cache keys. The first request populates the bucket with a permission-filtered response; subsequent hits from unrelated shares or anonymous clients receive that payload without any permission re-evaluation.\n\nThis is the web-cache pattern \"authorization-dependent response cached under an unsegmented key\" (cache key collision / missing authorization context in cache key, CWE-524 and CWE-639). Two adjacent read populations collide:\n\n- **Share to share:** `Share A` populates the cache, `Share B` reads `Share A`\u0027s scoped response.\n- **Share to anonymous** (and the reverse): any unauthenticated client hitting the same URL retrieves cached share-scoped data without presenting any token.\n\n## Affected\n\n- Config required: `CACHE_ENABLED=true` (any store: memory, redis, memcached) plus at least one active `directus_shares` row. This is not a default-on bug: `CACHE_ENABLED` ships as `false`. The cache is documented as a production performance setting, so operators who enable it are the ones affected.\n\n## Vulnerability class\n\n- **CWE-524:** Use of Cache Containing Sensitive Information\n- **CWE-639:** Authorization Bypass Through User-Controlled Key (the key here is the derived cache key, not the URL)\n- **OWASP API3:2023:** Broken Object Property Level Authorization\n\n## Impact\n\n- **Cross-share confidentiality breach.** Any share\u0027s filtered response can be served to a holder of a different share token, or to an anonymous request, that hits the same URL and query. Shares are advertised as a mechanism to distribute scoped, read-only access to specific items; this bug makes every cached share response readable by any other share-token holder who can reach the URL (the item-detail admin UI uses a predictable pattern).\n- **Anonymous request can read share data.** Anonymous requests also compute `user=null`. An anonymous client hitting `/items/articles?fields=*` after a share request has populated the cache receives the share\u0027s scoped payload with zero authentication.\n- **Password-protected share, derivative effect.** Password protection lives only at `shares.login` (JWT issuance). Once any share has populated the cache for a URL, an anonymous or alternate-share request to that URL retrieves the cached payload without exchanging the share password. This is the same cache-key collision surfacing as a password-protection bypass symptom, not a distinct mechanism.\n- **Persistence.** The leak persists for the `CACHE_TTL` window (commonly 5 to 30 minutes). `CACHE_AUTO_PURGE` clears on mutating writes to cached collections but does not purge per-user or per-share. With `CACHE_STORE=redis` (common in production) the poisoned bucket survives server restarts.\n- **No write impact.** The bypass is read-only.\n\nScope of the leak depends on the share\u0027s permission surface. A share with no backing role (`directus_shares.role = null`, the default) collapses visibility to the primary key, so the cache leaks only PKs. A share backed by a role with broader field access (the intended production setup for distributing useful content) leaks the full content that role can see on the scoped item.",
"id": "GHSA-c6w9-5g5j-jh2p",
"modified": "2026-07-20T21:48:15Z",
"published": "2026-07-20T21:48:15Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/directus/directus/security/advisories/GHSA-c6w9-5g5j-jh2p"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-61836"
},
{
"type": "WEB",
"url": "https://github.com/directus/directus/pull/27707"
},
{
"type": "WEB",
"url": "https://github.com/directus/directus/commit/7ba4efb97525d3af33570537c76e44baea767f13"
},
{
"type": "PACKAGE",
"url": "https://github.com/directus/directus"
},
{
"type": "WEB",
"url": "https://github.com/directus/directus/releases/tag/v12.0.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Directus: Authorization-dependent response served from unsegmented cache key"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.