Skip to content

ESI: make the private control on nonce blocks login-aware to stop empty nonces on public pages#1010

Open
robertstaddon wants to merge 1 commit into
litespeedtech:devfrom
robertstaddon:esi-private-nonce-login-aware
Open

ESI: make the private control on nonce blocks login-aware to stop empty nonces on public pages#1010
robertstaddon wants to merge 1 commit into
litespeedtech:devfrom
robertstaddon:esi-private-nonce-login-aware

Conversation

@robertstaddon

Copy link
Copy Markdown

Fixes #1009

Summary

When a nonce is registered with the private control (e.g. my_nonce_action private), the wp_create_nonce() override emits a private ESI include for every visitor. On a publicly cached page, that routes guest fragments into a private cache scope that does not exist for anonymous requests; they go empty on the Private Cache TTL cycle (~1800s by default).

Strip private from the nonce control at emission when ! Router::is_logged_in(), using the same predicate load_nonce_block() already uses to decide privacy is only needed for logged-in users. Full root-cause analysis, impact, and rejected alternatives are in the linked issue.

Changes

  • litespeed-cache.php — in the wp_create_nonce() override, drop private from the control before calling sub_esi_block() for non-logged-in requests.
if ( false !== strpos( $control, 'private' ) && ! \LiteSpeed\Router::is_logged_in() ) {
    $control = trim( preg_replace( '/\bprivate\b/', '', $control ) );
}

Why emission (not resolve)

See the issue for the full walkthrough. In short: load_nonce_block() already skips set_private() for guests; guest privacy is set earlier via the include's cache-control='private' attribute and _control=private in the signed URL — both fixed only at emission. A resolve-side change would require altering generic load_esi_block() and still could not rewrite the attribute baked into cached page HTML.

Safety / _hash

  • Logged-in behavior is unchanged (private on the include; load_nonce_block() still privatizes at resolve).
  • Guest nonces are uid 0 and shared across anonymous visitors — public ESI is correct.
  • _control is signed and validated symmetrically: guests omit it; logged-in users keep _control=private.

Test plan

  • Public page with a private-registered nonce, logged out: include has no cache-control='private'; nonce stays populated past Private Cache TTL (> 1 hour).
  • Same page, logged in: nonce still resolved privately (unchanged).
  • _hash validates for both guest and logged-in includes after the control change.

…gin-aware

A nonce registered with the `private` control is made private for every
visitor: it is emitted with cache-control='private' on the ESI include,
and at resolve load_esi_block() re-applies set_private() from the
_control URL param with no login check. load_nonce_block() only needs
privacy for logged-in users (it calls set_private() only when
Router::is_logged_in()), and a logged-out nonce is uid 0 / shared, so it
never needed to be private. On a publicly cached page a private fragment
has no private vary scope for an anonymous request, so it empties on the
server's Private Cache TTL cycle until the next purge.

Strip `private` from the nonce control for non-logged-in requests in the
wp_create_nonce() override, using the same Router::is_logged_in() check
load_nonce_block() uses. Logged-in behavior is unchanged, guest nonces
are safely shared via the public ESI path, and _hash validation is
unaffected because _control is signed and validated symmetrically.

Co-authored-by: Cursor <cursoragent@cursor.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