Skip to content

Harden dismiss-notice AJAX handler (nonce, capability, key allow-list) - #19

Merged
andypotanin merged 1 commit into
masterfrom
fix/dismiss-notice-authz
Sep 2, 2026
Merged

andypotanin merged 1 commit into
masterfrom
fix/dismiss-notice-authz

Conversation

@andypotanin

@andypotanin andypotanin commented Sep 2, 2026

Copy link
Copy Markdown
Member

What

Hardens the ud_bootstrap_dismiss_notice AJAX handler in lib/classes/class-bootstrap.php (registered at wp_ajax_ud_bootstrap_dismiss_notice):

  • check_ajax_referer( 'ud_bootstrap_dismiss_notice', 'nonce' ) — requires a valid nonce
  • current_user_can( 'manage_options' ) — the dismiss writes global state; previously any authenticated user (e.g. subscriber) could invoke it
  • Option key constrained via sanitize_key() + must match the dismiss_*_notice pattern — the only key shape define_splash_pages() reads back (lines 320/364); previously any arbitrary wp_options key could be written
  • Values sanitized (sanitize_key/sanitize_text_field)

Context

  • Reported through Patchstack responsible disclosure (report ec72be3a, publishes Oct 1 2026). Changelog wording intentionally avoids exploit detail until then.
  • Sibling handlers in this same release already carry check_ajax_referer() (e.g. class-errors.php dismiss_notices()); this one was missed. 1.3.3 fixed the related handler from CVE-2024-1385.
  • The handler has no callers in this library or in wp-stateless (notices dismiss via a GET link; ud-dismiss.js uses the separate ud_dismiss action), so no JS changes are needed.
  • changes.md gets the 1.3.5 entry so the tag can be cut right after merge.

Verification (live environment)

Tested on a real WP install (wp-stateless 4.4.1 active, this lib patched in):

  • subscriber + payload, no nonce → -1, target option untouched (previously: success:1 and the option was overwritten)
  • subscriber + valid nonce → "You are not allowed to do this action."
  • admin + valid nonce + out-of-pattern key (template) → "Invalid key", option untouched
  • admin + valid nonce + legit dismiss_wp_stateless_4_4_1_notice key → success:1, dismiss flow preserved
  • Same cases verified end-to-end over HTTP through the site's public URL, on both an existing (cloned production DB) and a fresh WordPress install
  • Ghost Inspector regression test added to the org suite (passes only when the endpoint returns -1)

Devin Review

Copilot AI balanced review requested due to automatic review settings September 2, 2026 03:22

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +617 to +620
if( ! $error && ( strpos( $option_key, 'dismiss_' ) !== 0 || substr( $option_key, -7 ) !== '_notice' ) ) {
$response['error'] = __( 'Invalid key', $this->domain );
$error = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Broad pattern permits unrelated option writes

Any dismiss_*_notice name passes validation without matching the current product. A compromised administrator can overwrite unrelated options with that shape.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused changes correctly implement the stated security controls without unresolved issues.

Pull request overview

Hardens the global notice-dismissal AJAX endpoint against unauthorized or arbitrary option updates.

Changes:

  • Adds nonce and manage_options authorization checks.
  • Restricts option keys and sanitizes stored values.
  • Documents the security fix for release 1.3.5.
File summaries
File Description
lib/classes/class-bootstrap.php Secures and validates notice-dismissal requests.
changes.md Adds the 1.3.5 security changelog entry.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused security controls are correctly implemented and consistent with the documented behavior.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@andypotanin

Copy link
Copy Markdown
Member Author

Re: Devin Review's 🟥 on the dismiss_*_notice pattern being broader than the current product — keeping it deliberately:

  1. It matches the security reporter's recommended fix (constrain the option name to a literal prefix). Pre-fix, any authenticated role could write any option; post-fix it requires manage_options, already a fully trusted role in WordPress (plugin editing, arbitrary option writes via core). The residual impact of writing an inert dismissal flag for another product is nil.
  2. Every product using this library registers the same action name, so on multi-product sites only one instance's handler runs. Pinning to the instance's own computed key (dismiss_wp-stateless_4_4_1_notice here) would silently break dismissal for every other product sharing the action — the prefix+suffix pattern is what keeps the handler functional across the fleet.
  3. The value payload is sanitized and structurally inert (dismissal state only).

@andypotanin
andypotanin merged commit 063bfd7 into master Sep 2, 2026
5 checks passed
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.

2 participants