Skip to content

Add optional read-only access token to mitigate CREEP (CVE-2025-36852)#13

Open
joh-klein wants to merge 1 commit into
nxcite:masterfrom
joh-klein:feat/read-only-token
Open

Add optional read-only access token to mitigate CREEP (CVE-2025-36852)#13
joh-klein wants to merge 1 commit into
nxcite:masterfrom
joh-klein:feat/read-only-token

Conversation

@joh-klein

Copy link
Copy Markdown

Implements #12.

What

An optional READ_ONLY_ACCESS_TOKEN (env or --read-only-access-token). When set:

  • GET /v1/cache/{hash}: accepted with either token
  • PUT /v1/cache/{hash}: requires SERVICE_ACCESS_TOKEN; the read-only token gets 403 Forbidden
  • unknown token: 401 as before

This enables the standard CREEP mitigation topology for shared caches: trusted branches (main/release) get the read-write token, untrusted jobs (PR builds) get the read-only one — they still benefit from cache hits but cannot pre-seed/poison entries that trusted builds will consume.

Design notes

  • Backwards compatible: without READ_ONLY_ACCESS_TOKEN, behavior is byte-for-byte unchanged.
  • Deliberately not support multiple and named service access tokens #4: no named tokens, no per-token buckets/prefixes. Exactly one privilege boundary (read vs write), which was the missing piece identified in the support multiple and named service access tokens #4 review.
  • Both tokens are always compared (constant-time ct_eq, as before), so response timing doesn't reveal which token matched.
  • Config validation refuses to start if the read-only token is empty or identical to the service token.
  • Nothing token-related is logged.
  • README gets a short "Protecting against cache poisoning" section (placed after the existing simple config docs — simple first, advanced later); docs/index.html env-var block kept in sync.

Testing

e2e against MinIO, all passing:

case result
PUT with RW token 202
GET with RW / RO token 200, content identical
PUT with RO token (new or existing hash) 403
GET missing hash with RO token 404
wrong / missing token, any method 401
PUT existing hash with RW token 409
/health without token 200
server start with RO token == RW token refused with clear error
server start with empty RO token refused with clear error
no RO token configured unchanged behavior; former RO token is 401

cargo build + cargo fmt clean.

🤖 Generated with Claude Code

A single read-write token means every cache consumer can also write,
so untrusted CI jobs (e.g. PR builds) holding the token can pre-seed
the cache entry for a hash a trusted branch will later compute — the
CREEP attack. The 409-on-existing immutability does not cover this:
the attack writes first, it never overwrites.

Add an optional READ_ONLY_ACCESS_TOKEN. The auth middleware accepts
either token for GET, but only SERVICE_ACCESS_TOKEN for writes; a
read-only token on a non-GET request gets 403 Forbidden. Both tokens
are always compared in constant time so timing does not reveal which
one matched. Config validation rejects an empty read-only token and a
read-only token equal to the service token.

Backwards compatible: without READ_ONLY_ACCESS_TOKEN set, behavior is
unchanged.

Closes nxcite#12

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
joh-klein added a commit to meddevo/nx-cache-server that referenced this pull request Jul 17, 2026
Nx rejects a bodyless 401 with 'Misconfigured remote cache endpoint:
Requests should respond with text/plain on 401s.' The auth middleware
returned bare StatusCode (empty body) for missing/invalid tokens (401) and
for read-only-token write attempts (403 - the PR nxcite#13 CREEP path). Route both
through ServerError so they carry a text/plain body. Adds a ServerError::Forbidden
variant and contract tests for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant