Skip to content

Fix CVE-2026-53739 (CSRF) & CVE-2026-53740 (XSS)#508

Merged
enricobattocchi merged 2 commits into
trunkfrom
claude/cve-2026-53739-53740-fix-b9qdjt
Jun 12, 2026
Merged

Fix CVE-2026-53739 (CSRF) & CVE-2026-53740 (XSS)#508
enricobattocchi merged 2 commits into
trunkfrom
claude/cve-2026-53739-53740-fix-b9qdjt

Conversation

@enricobattocchi

@enricobattocchi enricobattocchi commented Jun 11, 2026

Copy link
Copy Markdown
Member

Context

Two vulnerabilities were disclosed against Yoast Duplicate Post (all versions through 4.6):

  • CVE-2026-53739 (CSRF): the "You've successfully installed Yoast Duplicate Post!" admin notice could be dismissed for everyone through a forged request, because the dismiss handler verified neither a nonce nor the user's capabilities.
  • CVE-2026-53740 (stored XSS): the Classic editor's "scheduled to replace the original" notice printed a Rewrite & Republish copy's title and permalink without escaping. On supported WordPress (the plugin requires 6.8+) core neutralizes this at output, because wp_admin_notice() runs admin notices through wp_kses_post() (WP 6.4+), so the payload renders inert and script execution is only reachable on WordPress older than 6.4. The values are now escaped at the source so the plugin no longer relies on that core behavior.

Summary

This PR can be summarized in the following changelog entry:

  • Improves the security of the welcome notice dismissal by requiring a valid nonce and the manage_options capability.
  • Improves the security of the scheduled republish notice in the Classic editor by escaping the post title and permalink before output.

Relevant technical choices:

  • The dismiss handler now requires the manage_options capability — the same capability already required to see the notice (duplicate_post_show_update_notice()) — plus a nonce that is generated with wp_create_nonce(), sent with the existing AJAX request, and verified with check_ajax_referer().
  • The scheduled-republish notice now wraps the permalink in esc_url() and the title in esc_html(). The surrounding sentence already goes through esc_html__(), so only the interpolated link needed escaping.
  • This is defense-in-depth: WordPress core already strips executable markup from admin notices at output (wp_kses_post() via wp_admin_notice(), WP 6.4+), so the notice is not exploitable on the supported WP range (6.8+). The escaping also covers older cores that echo the message raw.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

Stored XSS (CVE-2026-53740)

Core (WP 6.4+) already strips <script> and event handlers from admin notices, so on a supported install this is verified by confirming the copy title is rendered as escaped text instead of being interpreted as HTML.

  1. On a single-site install with the Classic editor active, create and publish a post, then choose Rewrite & Republish on it.
  2. In the rewrite copy, set the title to <img src=x onerror=alert(1)> and schedule it for a future date.
  3. On the scheduled-confirmation notice ("… is now scheduled to replace the original …"):
    • Before the fix: the title's markup is interpreted by the notice and a broken <img> element renders. (No alert fires even before the fix: WordPress core strips the onerror handler at output.)
    • After the fix: the title is shown as literal text (<img src=x onerror=alert(1)>) and no element renders.
  4. Optional: view the page source to confirm the title appears HTML-encoded after the fix.

CSRF (CVE-2026-53739)

This verifies the notice can only be dismissed by a request that carries a valid nonce (what a normal click sends), and not by a forged request that lacks one.

  1. Make sure the notice is visible: log in as an administrator and go to Dashboard or Plugins. You should see the "You've successfully installed Yoast Duplicate Post!" notice. (If you've already dismissed it, re-enable it under Settings → Duplicate Post → Display by ticking Show welcome notice and saving, or with WP-CLI: wp eval "update_site_option('duplicate_post_show_notice', 1);", then reload.)
  2. Open your browser's developer tools (F12) and switch to the Console tab. You are logged in as admin, so your session cookies are sent automatically — this simulates a forged request that has the right action but no valid nonce.
  3. Paste this and press Enter to fire a dismiss request without a nonce:
    jQuery.post(ajaxurl, { action: 'duplicate_post_dismiss_notice' }, function (r) { console.log('response:', r); });
    (Optionally repeat with an invalid nonce by adding nonce: 'invalid' to the object.) The logged response should be 0 — the request was rejected.
  4. Reload the page. Expected: the notice is still there — the forged request did not dismiss it.
  5. Positive control: click the notice's × button (this sends the real request, with the nonce the page generated). The notice disappears, and after a reload it stays gone — confirming legitimate dismissal still works.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

  • The "You've successfully installed Yoast Duplicate Post!" admin notice and its dismissal (duplicate_post_dismiss_notice() AJAX handler in admin-functions.php).
  • The Classic editor scheduled-republish notice for Rewrite & Republish copies (src/ui/classic-editor.php).

UI changes

  • This PR changes the UI in the plugin. I have added the 'UI change' label to this PR.

Documentation

  • I have written documentation for this change. For example, comments in the Relevant technical choices, comments in the code, documentation on Confluence / shared Google Drive / Yoast developer portal, or other.

Quality assurance

  • I have tested this code to the best of my abilities
  • I have added unittests to verify the code works as intended

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label and noted the work hours.

Fixes CVE-2026-53739 and CVE-2026-53740.

@enricobattocchi enricobattocchi force-pushed the claude/cve-2026-53739-53740-fix-b9qdjt branch from 0d4837f to 0d240f3 Compare June 11, 2026 20:07
@coveralls

coveralls commented Jun 11, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 27382839654

Coverage increased (+0.07%) to 60.272%

Details

  • Coverage increased (+0.07%) from the base build.
  • Patch coverage: 2 uncovered changes across 1 file (8 of 10 lines covered, 80.0%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
admin-functions.php 6 4 66.67%
Total (2 files) 10 8 80.0%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
admin-functions.php 1 46.61%

Coverage Stats

Coverage Status
Relevant Lines: 2716
Covered Lines: 1637
Line Coverage: 60.27%
Coverage Strength: 7.34 hits per line

💛 - Coveralls

@enricobattocchi enricobattocchi force-pushed the claude/cve-2026-53739-53740-fix-b9qdjt branch 2 times, most recently from feb4bd1 to 2b65ddc Compare June 11, 2026 21:09
@enricobattocchi enricobattocchi changed the title Fix CVE-2026-53739 (CSRF) & CVE-2026-53740 (XSS) and related capability hardening Fix CVE-2026-53739 (CSRF) & CVE-2026-53740 (XSS) Jun 11, 2026
@enricobattocchi enricobattocchi added this to the 4.7 milestone Jun 11, 2026
@enricobattocchi enricobattocchi requested a review from Copilot June 11, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Addresses two reported security vulnerabilities in Yoast Duplicate Post by hardening the admin-notice dismissal flow against CSRF and escaping user-controlled values in the Classic Editor scheduled-republish notice to prevent stored XSS.

Changes:

  • Adds nonce + capability checks to the duplicate_post_dismiss_notice AJAX handler and passes a nonce from the notice UI.
  • Escapes permalink and title in the Classic Editor “scheduled to replace” notice.
  • Adds/updates unit tests covering the new CSRF protections and XSS escaping behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
admin-functions.php Adds nonce generation to the notice UI and enforces manage_options + nonce verification in the dismiss handler.
src/ui/classic-editor.php Escapes permalink (esc_url) and title (esc_html) before interpolating into scheduled-republish notice HTML.
tests/WP/Admin_Functions_Test.php Adds WP integration tests validating dismiss behavior for valid/invalid nonce and insufficient capability.
tests/Unit/UI/Classic_Editor_Test.php Updates existing tests to stub escaping and adds a regression test for malicious titles in scheduled notice output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread admin-functions.php
claude added 2 commits June 12, 2026 01:00
Add a nonce to the dismiss-notice AJAX request and verify both the
nonce and the manage_options capability in duplicate_post_dismiss_notice()
before updating the site option. Covered by integration tests asserting
the option only changes for an authorized user with a valid nonce.
…26-53740)

Wrap the permalink with esc_url() and the post title with esc_html()
before injecting them into the Classic Editor post-updated notice,
preventing stored XSS via a malicious Rewrite & Republish copy title.
Covered by a test asserting a hostile title is rendered as inert text.
@enricobattocchi enricobattocchi force-pushed the claude/cve-2026-53739-53740-fix-b9qdjt branch from 2b65ddc to ea9fbc2 Compare June 11, 2026 23:00
@enricobattocchi enricobattocchi merged commit fcf9628 into trunk Jun 12, 2026
27 checks passed
@enricobattocchi enricobattocchi deleted the claude/cve-2026-53739-53740-fix-b9qdjt branch June 12, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants