Skip to content

sessionCacheLimiter default breaks CSP nonces via 304 response nonce mismatch #2201

@adrianbj

Description

@adrianbj

Summary

The new $config->sessionCacheLimiter setting (3.0.258, commit processwire/processwire@ae20ea03) defaults to private_no_expire for guest users. This silently breaks sites using $config->cspNonce with strict-dynamic because it enables HTTP 304 responses that cause a nonce mismatch between the CSP header and the cached HTML body.

The Problem

When a browser caches an HTML page and later revalidates it (via If-None-Match or If-Modified-Since), the server responds with 304 Not Modified — sending new response headers while the browser reuses the cached response body.

For sites using CSP nonces, this means:

  • The CSP header contains a new nonce (generated fresh by $config->cspNonce)
  • The HTML body contains <script nonce="..."> tags with the old nonce (from the cached response)
  • The nonces don't match → every nonced inline script is blocked
  • With strict-dynamic, every external script loaded via nonced <script> tags is also blocked

The result is a completely broken page — no JavaScript executes at all.

Reproduction

  1. Use $config->cspNonce in a CSP header with strict-dynamic
  2. Add nonce attributes to <script> tags: <script nonce="<?=$config->cspNonce?>">
  3. Leave sessionCacheLimiter at its default (private_no_expire for guests)
  4. Visit a page, navigate away, then return — the browser revalidates and gets a 304
  5. All scripts are blocked; the browser console shows CSP violations for every script on the page

None of the Built-in Options Are Safe for CSP Nonces

Option bfcache CSP nonces Issue
nocache Broken (no-store) Safe Disables bfcache
private_no_expire Works Broken Allows 304s → nonce mismatch
private Works Broken Sends Last-Modified → enables 304s
public Works Broken Same 304 problem
Custom headers Works Safe Requires user to know the workaround

Workaround

Use custom headers that force revalidation without enabling conditional requests:

$config->sessionCacheLimiter = [
    'guest' => [
        'Cache-Control' => 'private, max-age=0, must-revalidate',
    ],
    'loggedin' => 'nocache',
    'admin' => 'nocache',
];

Combined with stripping the response validators that enable 304s:

header_remove('ETag');
header_remove('Last-Modified');

This preserves bfcache (no-store is not used) while ensuring the browser always receives a full 200 response with a matching nonce in both the CSP header and the HTML body.

Suggestions

  1. Document the incompatibility between private_no_expire/private/public and CSP nonces, so users of $config->cspNonce know to use custom headers.

  2. Consider a nonce-aware cache option that automatically strips ETag and Last-Modified when $config->cspNonce is in use, preventing 304 responses while preserving bfcache. For example, an option like 'private_no_revalidate' that sends Cache-Control: private, max-age=0, must-revalidate and removes the conditional request validators.

  3. Consider changing the guest default to something safe when CSP nonces are detected — or at minimum, log a warning when private_no_expire is used alongside $config->cspNonce.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions