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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions