Add critical security-update task with UI lockdown and admin alerts - #769
Open
tacoverdo wants to merge 7 commits into
Open
Add critical security-update task with UI lockdown and admin alerts#769tacoverdo wants to merge 7 commits into
tacoverdo wants to merge 7 commits into
Conversation
When a WordPress core security release is available (any patch release on the installed branch, e.g. 6.8.1 -> 6.8.2), the plugin now: - Publishes a top-priority, non-dismissable, non-snoozable "security-update" task that becomes the ONLY recommendation shown to users who can install it, until the update is installed. Editors, user to-dos and pending celebrations are unaffected. - Emails all administrators (update_core users) directly via wp_mail, once per offered version. - Pings the new progress-planner-saas/v1/security-update-alert endpoint (license key + remote nonce, same pattern as change-site-url) so the SaaS can email the registered subscriber, and exposes a security_updates block in the get-stats payload. Detection runs on the set_site_transient_update_core hook so it works from front-end cron without an admin visit. Task IDs are versioned per release; superseded or withdrawn offers are cleaned up silently, and installing the update (manually or via auto-update) completes the task. Also hardens rest_prepare_recommendation() against an undefined prpl_url meta index when meta is not registered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
Test on Playground |
Contributor
✅ Code Coverage Report
🎉 Great job maintaining/improving code coverage! 📊 File-level Coverage Changes (13 files)🆕 New Files
📈 Coverage Improved
ℹ️ About this report
|
Contributor
🔍 WordPress Plugin Check Report
📊 Report
❌ Errors (1)📁 readme.txt (1 error)
|
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
0 |
update_modification_detected | Plugin Updater detected. Detected code which may be altering WordPress update routines. Detected: _site_transient_update_core |
📁 classes/suggested-tasks/providers/class-content-review.php (4 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
232 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information. |
377 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information. |
381 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information. |
388 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information. |
📁 classes/suggested-tasks/data-collector/class-unpublished-content.php (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
103 |
WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in | Using exclusionary parameters, like post__not_in, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information. |
📁 classes/activities/class-query.php (2 warnings)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
71 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $table_name used in $wpdb->query()\n$table_name assigned unsafely at line 58. |
163 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $where_args used in $wpdb->get_results()\n$where_args assigned unsafely at line 153. |
📁 classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
111 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $query used in $wpdb->get_row()\n$query assigned unsafely at line 98. |
📁 classes/suggested-tasks/data-collector/class-terms-without-posts.php (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
120 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 118. |
📁 classes/suggested-tasks/data-collector/class-terms-without-description.php (1 warning)
| 📍 Line | 🔖 Check | 💬 Message |
|---|---|---|
108 |
PluginCheck.Security.DirectDB.UnescapedDBParameter | Unescaped parameter $query used in $wpdb->get_results()\n$query assigned unsafely at line 106. |
🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check
progressplanner.com will watch the wordpress.org releases feed and read each site's installed version from the existing get-stats payload (security_updates.installed_version), instead of sites pushing to a new endpoint. This removes the push endpoint's abuse surface entirely. A regression test now asserts the alert path makes no outbound HTTP requests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On multisite only super admins can update core, so the expected recipient list must be built from get_super_admins() there — matching what Security_Update_Monitor::get_recipients() correctly does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The version-check API only labels the newest release "upgrade"; a branch-behind site (e.g. 6.9.1 when 7.0.x is current) receives its same-branch security patch (6.9.7) with response "autoupdate". The response filter only accepted "upgrade", so exactly the most at-risk sites — those on older branches — never got the task or the alert. Accept both "upgrade" and "autoupdate" offers; cross-branch entries are still rejected by the same-major.minor rule, dev builds by the stability check. Verified against the live API shape for 6.9.1 (upgrade 7.0.4 / autoupdate 7.0.4 / autoupdate 6.9.7 -> detects 6.9.7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wp-admin Updates page only lists the latest release — get_core_updates() skips "autoupdate" offers — so a branch-behind site (6.9.1 with 7.0.x current) is only ever offered the next major there. The task actions now include a form that POSTs the exact branch version and locale to core's own update-core.php?action=do-core-upgrade handler: find_core_update() matches any offer in the transient (including autoupdate ones), so the user gets core's native upgrade flow pinned to e.g. 6.9.7, with the nonce, capability check, maintenance mode and filesystem-credentials handling all handled by core. The "Go to the Updates page" link remains as fallback. Adds Security_Update_Monitor::get_pending_security_update_offer() to expose the matched offer object (version + locale). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Light-touch advice phrased not to license delay: "make a fresh backup first — but do not postpone the update if you have none." The one-click button routes around wp-admin's Updates page, which is where WordPress normally shows its backup notice, so the task and email carry it instead. The email links to the wordpress.org backups documentation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard carries the one-click branch-pinned update button; the wp-admin Updates page only offers the latest major, so it remains the target only for sites that have not onboarded yet (their dashboard would show the welcome screen instead of the task). Also restructures the email body into separate translatable paragraphs and rewords the auto-update note so it fits both link targets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
When a WordPress core security release is available, put site owners on highest alert:
security-updatetask (priority 0, 2 points, not dismissable, not snoozable) becomes the only recommendation shown to users who can install it — including under "Show all recommendations" — until the update is installed.update-core.php?action=do-core-upgradehandler. This matters because the wp-admin Updates page never shows same-branch patches (get_core_updates()skipsautoupdateoffers) and would push a branch-behind user toward the next major instead.find_core_update()accepts any offer in the transient, so core's native upgrade flow (nonce, capability, maintenance mode, FS credentials) runs pinned to the branch patch. A "Go to the Updates page" link remains as fallback.update_coreusers) receive a directwp_mailalert, once per offered version. No email addresses leave the site, and the alert path makes zero outbound HTTP requests (covered by a regression test).get-statspayload gains asecurity_updatesblock (pending,installed_version,offered_version,last_alerted_version) so progressplanner.com can email the registered subscriber via the feed-driven approach (see below).The alert email
Sent as plain text, one individually addressed
wp_mailper recipient, translatable via theprogress-plannertextdomain. For an onboarded site (example: 6.9.1 offered 6.9.7):Sites that have not onboarded get the same email without the dashboard paragraph, with the link pointing to
wp-admin/update-core.phpinstead (their PP dashboard would show the welcome screen, not the task).Detection
Every patch release on the installed branch (e.g. 6.8.1 → 6.8.2) is treated as a security release. Cross-branch jumps (6.7.2 → 6.8.0) and alpha/beta/RC builds never trigger. Branch-behind sites are covered because WordPress serves a same-branch point-release offer whenever security fixes ship — note that once a newer major exists, that offer arrives with response
autoupdate(only the newest release is labeledupgrade), so detection accepts both response types. Verified against the live version-check API for a 6.9.1 site (upgrade 7.0.4/autoupdate 7.0.4/autoupdate 6.9.7→ detects 6.9.7).Detection hooks
set_site_transient_update_core, so it fires from front-end cron — no admin visit needed — with anadmin_initfallback.Lifecycle
security-update-6-8-2): each release is a fresh task with fresh points, even after an older one was completed.automatic_updates_complete.Deliberately not locked down
update_core(they can't act on it) keep their normal task list.userprovider)./progress-planner/v1/tasksendpoint.Also in this PR
rest_prepare_recommendation()hardened against an undefinedprpl_urlmeta index (exposed by the new tests when meta isn't registered).on_automatic_updates_complete()generalized to handle bothupdate-coreandsecurity-updatetasks.SaaS-side work (separate, on progressplanner.com): feed-driven subscriber email
No new endpoint. Instead, progressplanner.com watches the wordpress.org releases feed; when a patch release ships for branch X.Y, it polls connected sites' existing
get-statsendpoint and emails the subscriber of any site whosesecurity_updates.installed_versionis on branch X.Y below the new patch. Notes:installed_version+ the feed, not the site'spendingflag — a freshly polled site's own update check may not have run yet.installed_versionreads the running version and is always accurate.An earlier revision of this PR pushed a
security-update-alertping to the SaaS; it was removed (see commit history) because a push-triggers-email endpoint is an abuse surface the feed-driven design avoids entirely.Testing
pre_site_transient_update_coreand assert the single-card lockdown) and manual verification on a live site.🤖 Generated with Claude Code