Wpcomsh fatal-error: log recovery-mode entry from screen click#48525
Wpcomsh fatal-error: log recovery-mode entry from screen click#48525taipeicoder wants to merge 3 commits intotrunkfrom
Conversation
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! Wpcomsh plugin:
If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
Routes the fatal-error screen's "Enter recovery mode" link through a first-party signed redirect endpoint that emits `wpcomsh_fatal_recovery` to logstash before forwarding to a freshly-generated core recovery URL. Email-originated entries continue to use the bare core URL and don't log, so the new event measures only screen-originated recovery-link uptake. Refactors out shared helpers used by both this endpoint and the deactivator: HMAC sign/verify (`wpcomsh_fatal_sign_payload` / `wpcomsh_fatal_verify_payload`), per-key dedup with fail-open semantics (`wpcomsh_fatal_dedup_acquire`), and the WPCOMSH_Log dispatch funnel (`wpcomsh_fatal_emit_logstash_event`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4b895fa to
8ccf415
Compare
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
Code Coverage SummaryThis PR did not change code coverage! That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷 |
|
One thought on the HMAC piece: the So the worst case the HMAC currently prevents on the recovery side is: an admin gets tricked into clicking a link, and lands in their own recovery mode. That's reversible, gives the attacker nothing, and doesn't affect anyone else. Worth weighing whether it's worth the ~80 lines of crypto + cookie-bootstrap. For reference, WordPress core itself doesn't HMAC the recovery URL it ships in the email — it relies on the single-use So one alternative worth considering: drop the HMAC machinery on the recovery side only, render the wrapper as a plain Totally fine to land as-is too — just wanted to share the observation. |
|
One thought worth sharing on the recovery wrapper specifically: would it be better to use The two are essentially equivalent in security model — WP nonces bind to The one caveat is that Net effect: ~5 lines of nonce verification in place of ~95 lines of HMAC machinery, the recovery link reads as a normal authenticated WP action URL ( WDYT? |
Replace the custom HMAC on the recovery click endpoint with a WP nonce
(`wpcomsh_recover` action). The recovery endpoint runs after
pluggable.php loads via `wpcomsh_fatal_current_user_id`, so
`wp_verify_nonce` is callable there; the deactivator endpoint keeps its
HMAC because it has to verify before pluggable.php is available. Pin
`nonce_life` for the recovery action via a `PHP_INT_MAX`-priority
filter registered lazily at the mint and verify call sites, so the
tick used at mint time (screen render, plugins loaded) and verify
time (mu-plugin file load, plugins not yet loaded) always agree. Mint
under the cookie-resolved user via `wp_set_current_user` so an ambient
`$current_user` mismatch can't sign for the wrong session.
Failure paths now 302 to a path on the WP install root with the
recovery query args stripped, instead of letting WP render a normal
page response under `?wpcomsh_recover=1&…`. The redirect base comes
from `wp_parse_url( site_url('/'), PHP_URL_PATH )`, not
`$_SERVER['REQUEST_URI']`, so a crafted `//attacker.com/path` request
URI can't produce a cross-origin `Location:` header. Subdirectory
installs (e.g. example.com/wp/) bounce back into the install root.
Unrelated query params survive via `remove_query_arg` over
`$_SERVER['QUERY_STRING']`. The header is sent raw rather than via
`wp_redirect` so early bails (empty nonce, multisite) don't pull
pluggable.php just to bounce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the suggested approached. I don't personally have any strong opinions on either approach, so I went with yours. Although I don't think it ended up being simpler 🙂 |
Phan's PhanPluginNeverReturnFunction caught the closure: it always exits and has no return path. wpcomsh requires PHP 8.1+, so the native `never` return type is available — keeps the closure's exit-only contract visible to the type checker rather than relying on inferred void. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Proposed changes
fatal-recovery-redirect.php) that the fatal-error screen's "Enter recovery mode" link now points at instead of core's bare recovery URL. The endpoint verifies an HMAC bound toAUTH_SALT+LOGGED_IN_COOKIE, dedups on the signed URL, validates the user (live cookie +resume_plugins/resume_themescap), generates a fresh core recovery URL, emits awpcomsh_fatal_recoverylogstash event, and redirects.recovery_keysoption no longer accumulates a row per visiting admin during a sustained outage.WP_Recovery_Mode::get_link_ttl()flow (recovery_mode_email_rate_limit→recovery_mode_email_link_ttl→max($valid_for, $rate_limit)), each filter guarded against re-fataling.fatal-error-helpers.phpshared with the existing deactivator path:wpcomsh_fatal_sign_payload,wpcomsh_fatal_verify_payload,wpcomsh_fatal_dedup_acquire,wpcomsh_fatal_emit_logstash_event. The deactivator endpoint and the screen-render path adopt them, dropping ~50 lines of inline duplication.Related product discussion/links
Does this pull request change what data or activity we track or use?
Yes — adds a single new logstash event slug (
wpcomsh_fatal_recovery) under the existingatomic_extension_conflictfeature bucket. Properties shipped:site_url,atomic_site_id(when defined). Per-site dedup at 5-minute window. No new PII.Testing instructions
add_action('init', function () { trigger_error('boom', E_USER_ERROR); });)./?wpcomsh_recover=1&wpcomsh_exp=…&wpcomsh_sig=…→ core recovery URL → wp-login.php → wp-admin in recovery mode.wpcomsh_fatal_recoveryrow in logstash with the site's URL.wpcomsh_fatal_recoverylog row (email continues to use the bare core URL).?wpcomsh_recover=1&wpcomsh_sig=junk&wpcomsh_exp=…URL. Expected: silent no-op, no log, no redirect, no recovery-mode entry.active_plugins, redirect towp-admin/plugins.php.🤖 Generated with Claude Code