Skip to content

fix(security): stop caching an empty CSP derivation - #3474

Merged
kojiwakayama merged 1 commit into
mainfrom
fix/derived-csp-empty-read
Aug 8, 2026
Merged

fix(security): stop caching an empty CSP derivation#3474
kojiwakayama merged 1 commit into
mainfrom
fix/derived-csp-empty-read

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Derived origins have never worked in production. #3465 was promoted as what closes the loop for the ~100 projects #3417 broke. It closes it in preview only.

How it was found. I created a throwaway project (vf-csp-probe) that declares no security.csp and whose page references https://images.unsplash.com and https://cdn.jsdelivr.net, then deployed the same release to both environments:

img-src
preview 'self' images.veryfront.com cdn.veryfront.com data: cdn.jsdelivr.net images.unsplash.com
production 'self' images.veryfront.com cdn.veryfront.com data:

Same release, same source, different answer.

Root cause. getAllSourceFiles returns [] whenever its own file list is cold, scheduling an asynchronous warmup. Every pod is cold for a content version on the first request after a release. That first request therefore derived nothing — and deriveOnce wrote that emptiness into the per-content-version cache, which is by design never revisited, since the input is supposed to be immutable. The warm file list that arrived moments later was never consulted, and the release served the bare floor for the life of the pod. Preview only appeared to work because its file list happened to be warm by the time anything asked.

The extractor itself is fine — given those files it returns exactly those origins. The release genuinely contains the source. The whole failure was one line of caching.

Fix. An empty read is not an answer. Files read with no origins found stays cached (immutable for the content version); nothing read is a race and is retried.

This reverses a deliberate decision, pinned by the test caches the empty result too, so a broken adapter is not retried per request. That saving could not tell a broken adapter from a cold one, and the cold case is the common one — it was paid for with the feature not working at all. The cost coming back is small by construction: the empty path is a cache lookup that schedules a warmup, not a source read, and concurrent callers still collapse onto one attempt through the in-flight map. I rewrote that test to state the new contract and why it changed rather than deleting it.

Consequence for the rollout: VERYFRONT_CSP_ENFORCE must not be flipped until this ships. The derived origins that make enforcement survivable are not being served in production today, so enforcing now would reproduce #3417 exactly.

The three new tests fail against main and pass with the fix; I checked that explicitly. Lint, typecheck, fmt, docs and the full unit suite (3777) are green.

Summary by CodeRabbit

  • Bug Fixes
    • CSP derivation now retries when source files are temporarily unavailable or a read initially fails.
    • Successful derivations with no applicable origins continue to behave consistently without retaining incomplete results.
  • Tests
    • Added coverage for retries after empty or failed reads and for successful empty derivations.

Derived origins did nothing for hosted production projects -- the projects
the feature exists for. Verified against a probe project whose page references
two external image origins: preview served both in `img-src`, production
served neither, from the same release and the same source.

`getAllSourceFiles` answers `[]` whenever its own file list is cold, warming it
asynchronously afterwards. Every pod is cold for a content version on the first
request after a release, so the first request derived nothing -- and that
emptiness was written to the per-content-version cache, which is never
revisited. The warm file list that arrived a moment later was never read, and
the release served the bare floor for the life of the pod. Preview only
appeared to work because its file list happened to be warm by the time it was
asked.

An empty read is not an answer. Files read with no origins found is immutable
for a content version and still cached; nothing read is a race and is retried.

This reverses a deliberate earlier decision, pinned by a test, that cached the
empty result so a broken adapter would not be re-read per request. That saving
could not distinguish a broken adapter from a cold one, and the cold case is
the common one -- it was paid for with the feature not working at all. The cost
coming back is small by construction: the empty path is a cache lookup that
schedules a warmup rather than a source read, and concurrent callers still
collapse onto one attempt through the in-flight map.
@kojiwakayama
kojiwakayama requested a review from kwakayama as a code owner August 8, 2026 15:31
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1df39760-3098-426c-adb0-f8ac7f234709

📥 Commits

Reviewing files that changed from the base of the PR and between 9e57b05 and 3bc670a.

📒 Files selected for processing (2)
  • src/security/http/derived-csp-cache.test.ts
  • src/security/http/derived-csp-cache.ts

📝 Walkthrough

Walkthrough

The derived CSP cache no longer caches empty or failed source reads. Later requests can retry source loading. Tests cover retries, valid empty derivations, retained origins, and updated failed-read load counts.

Changes

Derived CSP cache behavior

Layer / File(s) Summary
Retryable source loading and cache validation
src/security/http/derived-csp-cache.ts, src/security/http/derived-csp-cache.test.ts
Empty source lists and source-read failures return EMPTY without caching. Tests verify subsequent retries, valid empty derivation caching, retained origins, and two failed-read load attempts.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: kwakayama

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing empty CSP derivations from being cached.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/derived-csp-empty-read

Comment @coderabbitai help to get the list of available commands.

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