Roadmap: Past and Future Planning β Integration Tests, WP 7.0 Prep, Collaboration, TDD, and Core Design
Updated July 15, 2026
- Planned Development Timeline β Immediate, short-term, medium-term, and later work phases
- Context β current state, CI matrix, and WP 7.0 status (counts in
docs/current-metrics.md) - 1. Integration Tests β Current integration coverage, coverage analysis, remaining gaps
- 2. WordPress 7.0 Prep β WP 7.0 GA shipped May 20, 2026; package metadata and Connectors gating work have shipped; see
docs/release-status.mdfor current release state - 3. Collaboration & Sudo β Multi-user editing, conflict resolution
- 4. Context Collapse & TDD β LLM confabulation defense, test-driven development
- Recommended Next Steps β Immediate, short-term, medium-term priorities
- Archived Execution Record (v3.1βv3.3) β historical audit/remediation record; current priorities live in this roadmap and
.planning/STATE.md - 5. Environment Diversity Testing β Apache, PHP 8.0, MariaDB, backward compat
- 6. Coverage Tooling β PCOV baseline established, full matrix deferred
- 7. Mutation Testing β Deferred until integration suite is fast enough
- 8. Exit Path Testing β
@runInSeparateProcessfor security-critical exit/die paths - 9. Code Review Findings β Triaged findings from line-verified code review
- 10. Core Sudo Design β Already achieved (13), to implement (6), to consider (4), discarded (5)
- 11. Feature Backlog β Two Factor lifecycle bridge, bridge discovery mode, Gutenberg, multisite/network policy, SBOM/testing follow-ups
- 11.1 Multisite Network Admin Tools β Network dashboard widget, super admin widget visibility controls, cross-site activity aggregation
- 11.2 Internal Admin Least-Privilege & Governance β dedicated Sudo capabilities, explicit grants, migration-safe rollout
- 12. Security Hardening Sprint β Completed hardening sprint plus post-v3.1.3 review remediation phases
- Appendix A: Accessibility β 15 resolved WCAG items (v2.2.0βv2.3.1)
A June 7, 2026 deep security review found two implementation weaknesses and two design hardening opportunities. Two are shipped; two remain open:
2FA failure countersβ Shipped in v3.2.0 β failure counters now reset only after full sudo activation succeeds; bad 2FA attempts continue accumulating toward lockout across repeated password-success / bad-2FA cycles.WPGraphQL Limited mode encoding bypassβ Shipped in v3.2.0 β JSON bodies, GET/formqueryparams, multipartoperations, batched payloads, parser edge cases, and persisted-operation fail-safe are covered by unit tests.- Request stash redaction β suffix-based (shipped v2.11.0): Stash redaction is
exact-match plus suffix-based, not exact-key only.
Request_Stash::is_sensitive_key()(includes/class-request-stash.php) lowercases each field name and matches it against an exact list (sensitive_field_keys()) and 28 secret-value suffixes (SENSITIVE_KEY_SUFFIXES,@since 2.11.0) viastr_ends_with. Residual: camelCase provider names whose lowercased form ends in no listed suffix β e.g.clientSecret,refreshToken,authorizationβ are still not redacted, because the list omits baresecret/tokento avoid false positives (e.g. matchingpageβpassword). Broadening to conservative pattern-based redaction plus custom-rule non-replay metadata was intentionally OUT OF SCOPE for v4.0.0 (REQUIREMENTS); tracked here as a future option, not an open gap. Governance evolutionβ Shipped in v3.2.0 β Dedicatedmanage_wp_sudo,view_wp_sudo_activity,export_wp_sudo_activity, andrevoke_wp_sudo_sessionscapabilities replace broadmanage_optionsgovernance.Recovery mode is uncontainedβ Contained June 13, 2026 (found June 13, 2026) βWP_SUDO_RECOVERY_MODEpreviously granted the mastermanage_wp_sudocapability to any logged-in user (no role gate), with no on-screen notice and no audit event while active. The security model and FAQ had also claimed a non-dismissible notice and agovernance.recovery_modeaudit event that were never implemented (seedocs/llm-lies-log.md#20). Fixed: the grant in bothwp_sudo_can()andwp_sudo_map_governance_meta_cap()is role-gated tomanage_options/manage_network_options; a permanent non-dismissible notice renders on the Sudo settings screen; and a newwp_sudo_recovery_mode_activeaudit hook fires (stored as a sampledrecovery_modeevent). Remaining follow-up: a scoped single-user recovery form (see Β§12.1 Phase R3).
See Β§12.1 for the phased plan and acceptance criteria.
- Two Factor lifecycle bridge β Gate upstream Two Factor recovery-code generation, TOTP setup/deletion, and profile-form provider changes behind WP Sudo. This is a concrete factor-lifecycle gap: WP Sudo can require password + 2FA for sudo, but currently does not gate creation or replacement of the recovery codes that can satisfy that 2FA step later. See Β§11 Feature Backlog for verified source notes and implementation scope.
Critical-event alerting bridgeβ Shipped onmain(#166) βbridges/wp-sudo-critical-alert-bridge.php, an optional mu-plugin (same inert-when-unavailable pattern as the existing bridges) that pushes a notification when a critical / high-severity hook fires, complementing the log-only Stream/WSAL bridges. It wireswp_sudo_capability_tampered,wp_sudo_escalation_blocked,wp_sudo_lockout, andwp_sudo_gated_actions_missing_builtin_rulesβ plus opt-in, throttledwp_sudo_recovery_mode_activeβ towp_mailout of the box, with thewp_sudo_critical_alert_dispatchfilter to send Slack/Teams/webhook viawp_remote_post(or capture for inline display where outbound network is unavailable, e.g. Playground). Includes per-event toggles (wp_sudo_critical_alert_events), a per-identity dedupe window, and a per-recipient hourly cap so an incident cannot flood the channel. Seedeveloper-reference.mdβ "Optional Critical-Event Alert Bridge". Distinct from the planned External Audit Mode (which changes the audit destination, not alerting).
Update "Tested up to" β
Done; readme.txt advertises WordPress 7.0. WP 7.0 GA shipped May 20, 2026.
Connectors matcher and release verification path β
Done across the v4.x release line: the registry-aware matcher shipped, the package metadata now targets WP 7.0, and manual Connectors credential-gating checks live in tests/MANUAL-TESTING.md. Current release/package truth is docs/release-status.md; do not duplicate volatile tag or version facts here.
Remaining Connectors-adjacent backlog should be tracked as future feature work, not as WP 7.0 prep debt.
- Release Confidence E2E workflow β Add a manual release-grade GitHub Actions entrypoint that runs the existing high-confidence targets together without making nginx multisite or SQLite mandatory on every PR: Apache/wp-env full Playwright E2E, nginx + MariaDB smoke, nginx + MariaDB multisite smoke, and Playground SQLite smoke. See
docs/release-e2e-confidence.md. (Shipped in #155.) - Stabilize the nginx-multisite release-confidence smoke lane β hardened, re-scoped into the gate. The
nginx-multisite-smokejob (MSTACK-01/02/03 intests/e2e/specs/multisite-stack-smoke.spec.ts) had never been green: it failed on Playwright element-not-stable / actionability timeouts (#submit,#wp-admin-bar-wp-sudo-active) on the heavier multisite stack β a test-robustness/rendering issue, not a behavioral or4.6.0-related failure. The failing run's trace (run #28804948034,release-confidence-nginx-multisite-report) confirmed the leading hypothesis:#wp-admin-bar-wp-sudo-activewraps the live sudo countdown timer (re-renders every second) and#submitsits below an async MU-plugin status check that reflows the form, so on a loaded runner neither box ever satisfies Playwright's two-consecutive-frames stability check even though a real user could click at any time; the reauth POST β redirect round-trips were also slower than the single-site default budget. Fix (shipped): aforceClickhelper (scrollIntoViewIfNeeded()+click({ force: true })) for those confirmed-visible/enabled targets, a multisite-sizedNAV_TIMEOUTfor the navigations, and thecontinue-on-error/ aggregate exclusion removed so the lane once again gatesrelease-confidence.yml. Each forced click still asserts its result, so a mis-landed click fails loudly. Verify with a manualrelease-confidence.ymldispatch (or localnpm run test:e2e:multisite:stack-smoke) before relying on the restored gate. - Psudo Lite / Sudo Lite reference baseline β Preserve the original Psudo idea as a deliberately small page-level reauthentication reference, and define Sudo Lite as the minimal action-gating baseline. Use
docs/sudo-lite/fundamentals-cross-check.mdduring WP Sudo reviews so the full plugin remains faithful to the simple invariants: fresh reauthentication, current-session binding, short trust windows, clear protected-action rules, safe redirects, and safe non-interactive failure behavior. - Roadmap intent: these lightweight references are not replacement products for WP Sudo. They are design anchors and audit tools for keeping the main project understandable and security-focused as coverage expands.
Playwright E2E test infrastructure β
Done β CI browser coverage across challenge, admin UI, replay, policy, multi-factor, and in-editor reauth flows; alternate-stack smoke lanes are in place. (Current E2E test count is canonical in docs/current-metrics.md.)
Phase B: Apache + MariaDB CI job β
Done β covered by the named wp-env Playwright lane.
-
Phase C: Manual testing checklist for managed hostsβ Substantially delivered by external PR #98 (merged 2026-06-21):tests/MANUAL-TESTING.mdnow has a "Release Environment Matrix Checklist" covering an Apache lane, a managed-WordPress-host lane (Pressable / WP Engine / Cloudways), and a minimum-supported-WordPress lane, with per-lane core-smoke section refs and the WP floor sourced fromdocs/release-status.md(not hardcoded). Maps to GSD Phase 15 (ENV-01β¦03). Remaining: run the matrix at each release. -
Phase D: Scenario blueprints for manual review β Grow the staged-state Playground blueprints (
blueprint-recovery-mode.json,blueprint-user-switching.json) only where the state is hard to reproduce by hand and review is visual/UX rather than behavioral (behavioral cases stay in the unit/integration/Playwright suites). Candidates, in priority order:- Lockout state β pre-seed failed attempts so the challenge page loads already locked-out with a live countdown. Hard to stage manually (needs 5 rapid failures); the countdown UI is the thing under review.
- Multisite network-admin β super-admin dashboard widget plus network settings. Multisite is painful to stand up locally; pairs with the medium-term Multisite Network Admin Tools.
- 2FA challenge UI β partial value only: Playground cannot deliver TOTP/email codes, so only the password step and the 2FA step's rendering are reviewable, not the full flow. Scope to a UI-render check, not an end-to-end flow.
- Scoped single-user recovery β deferred: build alongside Phase R3's
define( 'WP_SUDO_RECOVERY_MODE', <user_id_or_login> )form, not before the feature exists. - Rot guard (do this first): these blueprints install from
main.zipand break silently when an option key or admin URL changes. Add a headless boot-smoke CI lane via@wp-playground/clithat loads each blueprint and asserts the landing page renders, reusing the.github/playground/sqlite-stack-smoke.blueprint.jsonpattern. Land the smoke lane before expanding the blueprint set so new scenarios are covered from day one. - Tag-pinned copies at release (release-task): the current
blueprint-recovery-mode.jsonandblueprint-user-switching.jsoninstall frommain.zip, so they track a moving branch and only demo correctly while the feature lives onmain. At each release, add tag-pinned copies that install from the release zip (e.g.β¦/refs/tags/vX.Y.Z.zip), mirroring the existingblueprint.json(tag-pinned) vsblueprint-main.json(main) split, so published/demo links stay reproducible against a fixed version. Add this to the release checklist alongside the existing blueprint-version steps.
- Network Dashboard Widget β Cross-site session visibility and event aggregation for super admins (see Β§11.1)
- Super Admin Widget Visibility Controls β Network-level setting to restrict dashboard widget to super admins only
- Cross-Site Session Revocation β Network-wide session revocation for incident response
- Gutenberg block editor integration β β
Milestone A shipped (in-editor reauth, password path): a build-free
apiFetchmiddleware turns a gated action'ssudo_requiredREST 403 into an in-editor reauth modal (an earlier increment used a@wordpress/noticessnackbar link-out; the modal supersedes it for cookie-auth). Milestone A merged via PR #178 (2026-07-07): modal grant + re-dispatch, the 2FA-bypass invariant, stale-nonce recovery, owner-scoped concurrent re-dispatch, degradation guards, and the 2FA double-prompt fix. Milestone B (in-modal 2FA) is the next track β reviewed brief in.planning/gutenberg-editor-reauth-milestone-b-2fa-partial-brief.md. E2E coverage lives intests/e2e/specs/editor-reauth.spec.ts. - Network policy hierarchy for multisite β Super admins set minimum session duration and maximum entry-point policies; site admins can only tighten.
- Core Trac alignment: privileged-action confirmation + Multisite terminology β Map Core Trac #20140, #37593, and #39174 to Sudo's product language and gated-rule catalog. Deliverables: confirm coverage for password/email/user/role changes; identify missing network-level actions; standardize docs on βnetwork administratorβ for ordinary Multisite authority, βsuper adminβ only for Core's technical concept, and βbreak-glass recovery modeβ / βsudo sessionβ for Sudo's temporary elevation concepts. Progress (2026-06-21, external PRs): #97 standardized the docs on βbreak-glass recovery modeβ and added explicit disambiguation from core's
WP_Recovery_Mode(FAQ, security-model, readme.md, readme.txt); #96 renamed the dashboard event label RecoveryβBreak-glass to match. Remaining: the βnetwork administratorβ vs βsuper adminβ terminology pass and the network-level gated-action coverage review. - Session-store architecture follow-up β Evaluate and likely implement a dedicated sudo-session table, with the current recommendation favoring an authoritative table plus usermeta shadow writes. See
docs/session-store-evaluation.md. Internal admin governance hardeningβ Shipped in 3.2.0. Dedicatedmanage_wp_sudo,view_wp_sudo_activity,export_wp_sudo_activity, andrevoke_wp_sudo_sessionscapabilities replace broadmanage_optionsdefaults. Seedocs/archive/internal-admin-governance-spec.mdfor the archived design spec.- Deprecate and remove
compatibilitygovernance mode β Thewp_sudo_governance_mode = 'compatibility'DB option is a permanent security regression path: it lets anymanage_optionsholder administer Sudo settings, undoing the governance model.WP_SUDO_RECOVERY_MODE(requires filesystem access) is the intended break-glass β now hardened (role-gated, with notice + audit event; see below). Plan: fire_doing_it_wrong()+ persistent admin notice when compatibility mode is active in the next minor release; remove the option and the fallback branch in the next major. Containβ Shipped in v3.4.0 (June 13, 2026) β The grant in bothWP_SUDO_RECOVERY_MODEbreak-glasswp_sudo_can()andwp_sudo_map_governance_meta_cap()is role-gated tomanage_options/manage_network_options; a permanent non-dismissible notice renders on the Sudo settings screen; and a newwp_sudo_recovery_mode_activeaudit hook fires (stored as a sampledrecovery_modeevent). Remaining: a scoped single-user recovery form (define('WP_SUDO_RECOVERY_MODE', <user_id_or_login>)). See Β§12.1 Phase R3.
The v4.0.0 major has shipped. The sudo_can() deprecated alias and compatibility
governance mode were removed, minimum requirements were raised to WordPress 6.4 and PHP 8.2,
and integrator/release notes were published. Current release facts have advanced beyond
v4.0.0; use docs/release-status.md for the latest tag, runtime
version, and package metadata.
Future major-release planning should start as a new milestone rather than editing this completed v4.0.0 plan in place.
Any development phase that significantly changes the UI must include a README/readme screenshot refresh task in that phase's scope. Treat screenshots as phased assets: update them when the public docs would otherwise become misleading, and expect to redo the set after later major UI changes.
- Client-side modal challenge (UX like GitHub) β
.needs-sudoCSS class on forms, JS intercepts submit, inline password prompt. Significant complexity. Likely a milestone unto itself. - REST API sudo grant endpoint β
POST /wp/v2/sudofor headless clients. Enables interactive sudo flow for SvelteKit/Next.js apps. - Per-session sudo isolation β Integration with
WP_Session_Tokensfor per-browser isolation. - SSO/SAML/OIDC provider framework β Provider interface parallel to existing 2FA hooks.
- Phase D: Docker Compose with switchable stacks
- Third-party bridge discovery mode β Report-only scanner/assistant for plugin AJAX, admin-post, REST, and generic dispatcher entry points; useful for bridge coverage without turning WP Sudo into a generic hook firewall.
All 5 phases shipped. Identified by independent assessments from Codex, Gemini, and Claude (March 2026).
P1 β Request Stash data minimizationβ Phase 1P1 β Upload-action coverageβ Phase 1P1 β Non-blocking rate limitingβ Phase 2P2 β Rule-schema validationβ Phase 3P2 β MU loader path resilienceβ Phase 3P3 β WPGraphQL persisted-query strategyβ Phase 4P3 β WSAL sensor extensionβ Phase 4IP + user multidimensional rate limitingβ Phase 5
The v3.0.0 release consolidates Phases 7β9 work plus pre-release fixes:
Connectors API gatingβ β Gates/wp/v2/settingswrites containingconnectors_*_api_keyfieldsPolicy presetsβ β One-click Normal, Incident Lockdown, and Headless Friendly presetsRequest / Rule Testerβ β Diagnostic panel for evaluating request shapes against gate rulesSettings UI revisionβ β Tabbed navigation, dropdown policies, improved preset UXEvent_Store persistence layerβ βwpsudo_eventstable with 14-day retention, cron pruning, SQLite compatSession Activity Dashboard Widgetβ β Active sessions, recent events, policy summary for site admins
Multi-dimensional rate limiting (IP + user)β implemented β Per-IP tracking via transients alongside per-user tracking, combined lockout policy, and the triggering IP address added as the thirdwp_sudo_lockouthook argument.
The operator tooling tranche shipped in v2.12.0.
WP-CLIβ implemented (wp sudosubcommandswp sudo status,wp sudo revoke [--user=<id>],wp sudo revoke --all)Stream bridgeβ implemented (bridges/wp-sudo-stream-bridge.php)Publicβ implemented (session check + challenge redirect helper for third-party integrations)wp_sudo_check()/wp_sudo_require()API
Action Registry schema validationβnormalize_filtered_rules()validates and normalizeswp_sudo_gated_actionsfilter output; malformed rules dropped fail-closed.MU loader resilienceβ basename/path resolution uses explicit fallback chain (WP_SUDO_PLUGIN_BASENAMEβ derived β canonical); diagnostic action on unresolved paths.WPGraphQL persisted-query strategyβwp_sudo_wpgraphql_classificationfilter enables external mutation classification for persisted-query setups;str_containsheuristic preserved as fallback.WSAL sensor bridgeβbridges/wp-sudo-wsal-sensor.phpmaps 11 audit hooks to structured WSAL events (IDs 1900001β1900011); inert when WSAL absent. (The audit-hook total has since grown β seedocs/current-metrics.md; newer hooks likewp_sudo_escalation_blockedare not yet bridged.)
WebAuthn gating bridgeβ shipped v2.10.0:bridges/wp-sudo-webauthn-bridge.phpgates security key registration and deletion AJAX endpoints (webauthn_preregister,webauthn_register,webauthn_delete_key) viawp_sudo_gated_actionsfilter. Prevents silent key registration from a hijacked session.WP 7.0 REST error notice CSS fixβ shipped v2.10.0: scoped#application-passwords-section .notice.notice-errorselector restores background in WP 7.0CI integration test fixesβ shipped v2.10.0: fixed Challenge constructor arg order in ExitPathTest, addedset_current_screen()for admin context in CI
Expire sudo session on password changeβ shipped v2.8.0: hooksafter_password_resetandprofile_update; meta-existence guard prevents phantom audit eventsWPGraphQL conditional displayβ shipped v2.8.0: settings, help tab, and Site Health adapt when WPGraphQL is inactiveApache dev testingβ v2.8.0 verified on Apache (Local by Flywheel): single-site + multisite subdomain, REST API gating, Application Password auth withHTTP_AUTHORIZATIONpassthrough confirmed working
Login grants sudo sessionβ shipped v2.6.0:wp_loginhook callsSudo_Session::activate(); opt-out filter added in 3.3.0 (the original "mirrors Unix sudo" rationale was wrong β see llm-lies-log #17)Gateβ shipped v2.6.0: closes session-theft β password change β lockout attack chainuser.change_passwordGrace period (two-tier expiry)β shipped v2.6.0: 120 s grace window after expiry; session-token-verified, deferred cleanup
WPGraphQL surface gating (Disabled / Limited / Unrestricted)β shipped v2.5.0, fixed v2.5.1βv2.5.2Cross-origin headless mutation bypassβ fixed v2.5.2 (SvelteKit testing revealed unauthenticated mutations passed through)Per-app-password policy dropdownβ fixed v2.5.2 (was silently broken since v2.3)Security hardening (Opus audit)β fixed v2.5.2: MU-plugin AJAX, app-password AJAX, user.promote ruleWPGraphQL headless authentication boundaryβ documented v2.5.2Abilities API (WordPress 6.9+)β documented v2.5.1: covered by existing REST API (App Passwords) policy
- PHP 8.0β8.4 for unit tests; targeted integration coverage for WordPress 6.2, 6.7, and 7.0 GA; single-site + multisite + PCOV coverage
This is a living document covering accumulated input and thinking about the strategic challenges and priorities for WP Sudo.
Current project state (as of July 15, 2026):
- Current release state is canonical in
docs/release-status.mdβ as of 2026-07-15, the latest tagged release isv4.6.0(cut 2026-07-06) and the plugin is not currently published to the WordPress.org plugin repository. Publication is intentionally delayed/on hold, but the repository should remain submission-ready;readme.txtStable tag is package/future-publication metadata until WordPress.org publication. - Current test and size counts are centralized in
docs/current-metrics.md. - CI pipeline: unit tests on PHP 8.0β8.4; integration tests on PHP 8.0/8.1/8.3; WordPress 6.2, 6.7, and 7.0 GA; single-site + multisite; MySQL 8.0 plus one MariaDB lane; PCOV coverage job; Playwright E2E suite (current test count in
docs/current-metrics.md) - WordPress 7.0 GA shipped on May 20, 2026, and the forward lane is now pinned to the final 7.0 release. See
docs/release-status.md.
Status: Complete. The integration test suite shipped in v2.4.0 (55 tests) and expanded in v2.4.1 (73 tests). CI now runs targeted compatibility lanes across PHP 8.0/8.1/8.3 and WordPress 6.2, 6.7, and 7.0 GA with single-site + multisite coverage. The analysis below is preserved for context on what drove the test design.
- Request matching across all 7 surfaces (98 GateTest methods)
- Session state machine, token crypto, rate limiting
- Hook registration and filter application
- Policy enforcement (DISABLED/LIMITED/UNRESTRICTED)
- Upgrader migration logic
- Settings sanitization and defaults
These gaps have been closed by the integration suite:
| Gap | Integration coverage (v2.9.2) |
|---|---|
| Cross-class workflows (Gate β Challenge β Session β Stash) | ReauthFlowTest β 4 end-to-end tests |
| Request stash replay | RequestStashTest β 7 tests including transient TTL |
Real wp_check_password() |
SudoSessionTest β 10 tests with real bcrypt |
| Transient TTL enforcement | RequestStashTest, SudoSessionTest |
| Two Factor plugin interaction | TwoFactorTest β 7 tests with real provider |
| Database state after migrations | UpgraderTest β 4 tests against real options/meta |
| REST API with real auth | RestGatingTest β 7 tests with cookie and app password auth |
| AJAX gating | AjaxGatingTest β 12 tests covering all 7 declared AJAX actions |
| Audit hooks | AuditHooksTest β 11 tests across CLI, Cron, XML-RPC, REST |
| Rate limiting | RateLimitingTest β 6 tests with real user meta |
| Multisite isolation | MultisiteTest β 5 tests |
| WPGraphQL surface gating | WpGraphQLGatingTest β 16 tests (policy modes, mutation detection, bypass filter) |
| Exit paths and grace window | ExitPathTest β 9 tests (REST, AJAX, WPGraphQL, admin redirect, challenge auth, grace window) |
| Uninstall cleanup | UninstallTest β 2 tests (single-site + multisite) |
| Login grants sudo | LoginSudoGrantTest β 3 tests |
| Password change expiry | PasswordChangeExpiryTest β 8 tests |
Not addressable with PHPUnit:
- Cookie/header behavior β
setcookie()still guarded byheaders_sent()check. Real httponly/SameSite attributes require browser-level testing (Playwright/Cypress). - Hook timing and priority β no automated test verifies
admin_initpriority 1 ordering relative to other plugins. Covered by manual testing guide. - Admin UI rendering β visual correctness tested manually, not automated.
Addressable β potential PHPUnit improvements:
| Opportunity | Current state | Value |
|---|---|---|
Admin class (1,244 lines) has 55 unit tests but no integration tests.tests/Integration/AdminTest.php covering defaults, persistence, sanitization clamping, per-app-password overrides, and multisite site_option storage. |
||
_wpnonce fallback |
X-WP-Nonce header only.$_REQUEST['_wpnonce'] (mirrors WP core rest_cookie_check_errors()). 2 unit tests in GateTest.php. |
|
tests/e2e/specs/mu-plugin.spec.ts exercises the real AJAX install/uninstall via the Settings page: MUPG-01 (install β "Installed"), MUPG-02 (success icon), MUPG-03 (uninstall β "Not installed"), plus a 403-without-session check. |
||
tests/Integration/PluginLifecycleTest.php (PR #121): single-site activation (cap strip, governance-cap grant, flag, cron), deactivation (restore, clear, preserve settings/meta), round-trip, idempotency, and multisite network activation. |
||
@runInSeparateProcess exit paths / Set-Cookie output |
Set-Cookie output.Set-Cookie): cookie.spec.ts COOK-01/02/03 assert httpOnly / SameSite=Strict / path=/ on activation; admin-bar-deactivate.spec.ts ABAR-01 asserts the token cookie is cleared on the deactivation exit path; admin-bar-timer.spec.ts TIMR-04 covers time-based expiry. Only Secure over HTTPS remains untestable on the http wp-env stack. |
Deferred to browser-level testing (Playwright/Cypress):
| Scenario | Why browser-level |
|---|---|
| Challenge page cookie attributes (httponly, SameSite, Secure) | setcookie() output not capturable in PHPUnit |
| Admin bar countdown timer JS accuracy | Requires real DOM + setInterval |
| MU-plugin install button AJAX flow | Button click β AJAX β file copy β status update |
| Block editor snackbar integration (future) | Requires @wordpress/notices API in browser |
| Challenge page keyboard navigation | Real focus management needs browser DOM |
Status: Complete. WP 7.0 GA shipped May 20, 2026. All RC and final signoffs are recorded in
tests/MANUAL-TESTING.md.Tested up to: 7.0ships in v3.3.0. Therewrite_role_error()/render_role_error_notice()workaround (Trac #64690) removed in v3.4.0. The Connectors registry-aware matcher remains as follow-up work.
| Change | Impact | Action needed |
|---|---|---|
| PHP minimum raised to 7.4 (dropping 7.2/7.3) | WP Sudo requires PHP 8.0+. No impact. | None. Already ahead of the curve. |
| Always-iframed post editor | All blocks render in iframe. WP Sudo's admin UI gating does not touch the block editor β it intercepts admin_init actions, not editor saves. |
Low risk. Verify the challenge page CSS still works inside the admin chrome. |
| Admin visual refresh (DataViews, design tokens, Trac #64308) | Settings β Sudo page uses standard settings_fields() / do_settings_sections(). If WP 7.0 reskins these, our page gets the new look for free. |
β Completed against WP 7.0 pre-release and GA builds; continue ordinary visual checks before future compatibility bumps. |
| Fragment Notes + @ mentions | Extends 6.9 Notes (block-level comments). No auth surface β notes are post meta. | No impact on WP Sudo. |
| Abilities API expansion | New REST surface for AI agents. 3 read-only core abilities in WP 7.0. Abilities use permission_callback (typically current_user_can()). Not gated by WP Sudo. |
No action for 7.0. Existing REST surface interception already covers /wp-abilities/v1/ routes. When destructive abilities appear (DELETE on /run), add a REST rule to Action_Registry. See docs/abilities-api-assessment.md. |
| WP AI Client merge proposal | Provider-agnostic AI API. Includes REST/JS layer. | No immediate impact. If merged, AI model calls routed through REST are covered by existing Gate. Monitor. |
| WordPress MCP Adapter | Translates Abilities into MCP tools for AI agents (Claude, Cursor, etc.). Calls abilities through the same REST endpoints. | No new surface. MCP Adapter is a REST consumer β covered by existing Gate::intercept_rest(). Same gating strategy as Abilities API. See docs/abilities-api-assessment.md. |
| Viewport-based block visibility | Editor-only. No auth surface. | No impact. |
| Trac #64690 β Bulk role-change error message (ticket) | β
Core shipped the fix in WP 7.0 GA. rewrite_role_error() and render_role_error_notice() removed in v3.4.0; corresponding unit tests deleted. |
Done. |
Install WP 7.0 Beta 1 on Local or Studio dev siteβ done (February 19, 2026)Run the manual testing guide against WP 7.0 pre-release buildsβ done; all 15 sections PASSVisual check: settings page, help tabs, admin bar timer, challenge interstitial, admin noticesβ done; all pass against refreshed admin chromeRunβ passing through WP 7.0 GA; CI covers the current 7.0 lanecomposer testRepeat manual verification on each later RC build and on the final releaseβ Done β WP 7.0 GA shipped May 20, 2026; final signoff recorded intests/MANUAL-TESTING.md.Keep the standard local verification set green for each RC/GA checkpointβ Done.Update version references when WordPress 7.0 final shipsβ Done in v3.3.0 βTested up to: 7.0inreadme.txt.Removeβ done (v3.4.0);handle_err_admin_role()workaroundrewrite_role_error()andrender_role_error_notice()removed after Trac #64690 confirmed in WP 7.0 GA.
Problem (verified against core trunk): the connectors.update_credentials rule
identifies connector-secret writes to /wp/v2/settings by a name convention β
is_connector_api_key_setting_name() matches ^connectors_[a-z0-9_]+_api_key$
(includes/class-action-registry.php). But WP 7.0 derives setting names from the
connector registry, and they do not all fit that shape: core's own defaults include
a connector whose key is wordpress_api_key (wp-includes/connectors.php), and
register_connector() lets plugins set an arbitrary authentication.setting_name.
A write to such a key is a registered connector secret that the current regex does
not gate β a false negative on exactly the credential-replacement threat the
security model advertises. (The regex can also false-positive on a benign option
that happens to fit the pattern.)
Fix: make the registry the source of truth.
- Primary: when
function_exists( 'wp_get_connectors' )(WP 7.0+), enumeratewp_get_connectors(), collectauthentication.setting_namefor every connector withauthentication.method === 'api_key', and gate any settings write whose params touch one of those keys. Core shape:{ id => { authentication: { method, setting_name } } }. - Fallback: keep the regex when the registry function is absent or returns empty (older WP, or connectors not yet initialized at match time) β fail toward gating.
- Files:
request_contains_connector_api_key()/is_connector_api_key_setting_name()and the rule definition inincludes/class-action-registry.php.
Design-review questions (security-sensitive + execution-context-dependent): is the
registry populated when the gate evaluates (REST permission_callback / admin_init)
relative to when _wp_connectors_init fires? Cache the setting-name set per request;
verify the full set of core default setting_names against trunk before coding;
multisite behavior.
Tests: a registered connector with a non-connectors_* setting_name (e.g.
wordpress_api_key) is gated; registry-unavailable falls back to the regex; a benign
non-connector key is not over-gated. Effort: LowβMedium.
The Abilities API (WP 6.9+) and the WordPress MCP Adapter are the first new admin action surfaces WordPress has added since Application Passwords (WP 5.6). The MCP Adapter translates registered abilities into MCP tools for AI agents β it calls the same REST endpoints, so both are covered by the same gating strategy.
Currently, all three core abilities are read-only (GET on /run). If a destructive
ability appears (e.g., a future core/delete-plugin using DELETE on /run), WP Sudo
would need to intercept it.
Recommended approach: Add a REST rule to Action_Registry matching destructive
ability runs. The existing Gate::intercept_rest() already intercepts all REST
requests via rest_request_before_callbacks β no new surface type is needed. A new
ability surface type would only be warranted if abilities become a separate
first-class non-REST execution surface in plugin/core practice. Core already has a PHP
execution path via WP_Ability::execute(), but that remains a monitor-only concern for
now rather than justification for a new surface type.
For full analysis, trigger conditions, and example rules, see
docs/abilities-api-assessment.md.
Notes (confirmed, shipping): Asynchronous block-level and fragment-level comments. These are post meta. They don't trigger admin actions. No sudo impact.
Real-time co-editing (experimental/uncertain): If it ships, multiple users edit the same post simultaneously with presence cursors. WordPress VIP already has this as a managed feature. For core, the technical challenge is making it work without WebSocket infrastructure on shared hosting.
WP Sudo's session is per-user, per-browser. It does not lock content or create cross-user state. So:
-
User A triggers a gated action (e.g., activating a plugin from the Plugins page while User B is editing a post). User A gets the challenge interstitial. User B is completely unaffected β they're on a different admin page.
-
Both users are in the post editor, User A triggers sudo from the admin bar shortcut (Ctrl+Shift+S). User A sees the challenge modal. User B sees nothing. Sudo session tokens are bound to User A's user meta and browser cookie. No cross-user interference.
-
The real edge case: collaborative editing of a gated resource. Today, WP Sudo gates admin actions (plugin activate, user delete, settings changes), not content saves. Post editing is not a gated action. So even with real-time co-editing, WP Sudo doesn't intercept content saves and no conflict arises.
-
Future edge case: if WP Sudo ever gates content actions (e.g., publishing, trashing, or deleting posts). In a co-editing scenario, User A publishes β sudo challenge β User B sees... what? The post is in a "pending publish" state from User B's perspective. This is a UX design question, not a technical one. The answer is probably: don't gate content saves. Gate the destructive actions around content (trash, delete, capability changes) where concurrent editing isn't relevant.
Collaboration features in WP 7.0 don't create conflicts with WP Sudo because:
- Notes are post meta, not admin actions
- Co-editing is content editing, not admin actions
- Sudo sessions are per-user, not per-resource
- The plugin gates admin operations, not content operations
Monitor for: destructive abilities exposed via REST, and any future core features that let one user's admin action affect another user's in-progress work.
WP Sudo is already past 13k lines of PHP. In an LLM-assisted workflow, context collapse means: the model can't hold the full codebase in context, starts making changes that conflict with code it hasn't read, invents function signatures instead of looking them up, and introduces subtle regressions.
This is exactly how the confabulation errors happened β training-data guesses substituted for verifiable facts. The same failure mode applies to the plugin's own code as it grows.
1. TDD β the single most effective mitigation
A "tests first, always" rule works because:
- It forces reading the existing code before writing new code (you need to know what to assert against)
- It catches regressions immediately, even when the LLM can't hold the full codebase in context
- It creates a machine-verifiable contract β the tests don't care whether the code was written by a human or an LLM
- It prevents the "lazy shortcut" failure where generated code looks right but doesn't actually work
2. CLAUDE.md as the architectural single source of truth
Already started. The Architecture section in CLAUDE.md is the most important defense against context collapse β it tells the LLM what exists without requiring it to read every file. Keep expanding it as new classes/surfaces are added.
3. Small, focused commits
Already practiced. Each commit should touch one concern. This limits the amount
of context needed per change and makes git diff reviewable.
4. PHPStan level 6 as a guardrail
Already in place. Static analysis catches type mismatches, undefined methods, and wrong argument counts β exactly the errors an LLM makes when it invents function signatures.
5. Pre-commit test gate
Already in Commit Practices ("Always run tests before committing"). Could be enforced with a git pre-commit hook, but the CLAUDE.md instruction is sufficient for LLM-assisted work since Claude follows it.
These are workflow tools for managing LLM context across sessions:
- Amp β context management for Claude Code sessions
- Beads β structured context passing between LLM calls
They address a real problem (session continuity), but they're additive tooling, not fundamental mitigations. TDD + CLAUDE.md + PHPStan address the root cause: the LLM writes code it can't verify. Tests verify it mechanically. Static analysis catches type-level errors. CLAUDE.md provides architectural context without needing to read every file.
If the project grows to 30k+ lines, context management tools become more valuable. At 13k lines, the bottleneck is verification (tests, linting, static analysis), not context retrieval.
Steps 1β16 completed in v2.4.0βv2.14.x development. Updated March 24, 2026.
Add TDD requirement to CLAUDE.mdβ done (v2.4.0)Install WP 7.0 Beta 1, run manual testing guideβ done (v2.4.0)Scaffold integration test harnessβ done (v2.4.0, 55 tests)Write first integration testsβ done (v2.4.1, 73 tests)Visual review against 7.0 admin refreshβ done (v2.4.0)WPGraphQL surface gatingβ done (v2.5.0βv2.5.2)Abilities API coverage documentedβ done (v2.5.1)Updateβ done (v3.3.0)Tested up towhen WordPress 7.0 final shipsCore design features β login=sudo, gate password changes, grace periodβ done (v2.6.0)Security hardening sprint β stash redaction, upload-action gating, non-blocking rate limitingβ done (v2.10.2βv2.11.0)Rule-schema validation and MU loader resilienceβ done (v2.11.0)WSAL sensor extension and GraphQL persisted-query strategyβ done (v2.11.0)Plan environment diversity testingβ done (see section 5)Multi-dimensional rate limiting (IP + user)β done (v2.13.0)Playwright E2E test infrastructureβ done; browser coverage and alternate-stack smoke lanes are in placeApache + MariaDB CI jobβ done; covered by the namedwp-envPlaywright lane
With the WordPress 7.0 compatibility release and v3.4.0 hardening work closed, the recommended implementation order is:
- Deprecate
compatibilitygovernance mode (β resolved) β Two paired governance security-debt items. (a) β Resolved in v4.0.0: rather than a deprecation cycle, thecompatibilitymode and thewp_sudo_governance_modeoption were removed outright in 4.0.0 β governance is always strict, and the inert option is auto-removed with a one-time dismissible notice and awp_sudo_inert_governance_mode_detectedaudit event. (b) β Shipped in v3.4.0: the break-glass path that replaces it is hardened β the grant is role-gated to administrators, a non-dismissible notice renders, and thewp_sudo_recovery_mode_activeaudit hook fires (stored as a sampledrecovery_modeevent), so it is explicit, auditable, and bounded (see Β§12.1 Phase R3).WP_SUDO_RECOVERY_MODEis the only supported break-glass path going forward, and it is now sound; compatibility mode can be removed once (a) lands. Effort: Low (deprecation remaining) - Monitor and tune E2E CI acceleration without reducing release assurance
- Explicit CI groups now split the former long challenge shard across behavior-focused jobs
- Keep the full Playwright suite available for release-grade confidence, but shorten normal feedback loops
- Measure grouped runtime before deciding whether cache/image/policy optimizations are still worth the complexity
- Treat worker-level parallelism as a later step that requires explicit safety rules
- Effort: Low to medium
Refresh README screenshots for the current UIβ done onmain- Refreshed the WordPress.org-style screenshot assets for Settings -> Sudo, Gated Actions, Rule Tester, Access, the Session Activity dashboard widget, and break-glass recovery mode
- Added a compact screenshot gallery to the GitHub README and updated the
readme.txtcaptions - Expect to redo this again after major UI changes; keep future passes focused on making current public docs honest
- Strengthen release-only environment checks instead of broadening required CI
- Add the managed-host/manual environment checklist promised in section 5
- Keep SQLite as smoke/release assurance, not a required merge gate
- Add breadth only where a real compatibility signal is missing
- Effort: Low to medium
- Design Gutenberg Block Editor reauthentication UX
- Treat this as the most important next major product feature, even though it is the largest UX/design lift
- Define challenge transport, snackbar/notices behavior, autosave/editor-state safety, replay semantics, and test strategy before implementation
- Use the design phase to decide what needs Playwright coverage, whether any build tooling is required, and how failures should recover without losing editor work
- Effort: High
- Build the Sudo Activity screen MVP as a modest built-in visibility layer
- Keep it focused on recent Sudo events, short retention, and support/debugging value
- Do not make it a full audit-log, alerting, export, or notification product
- Prepare the UI for External Audit Mode so Stream/WP Activity Log sites can delegate persistence and notifications
- Effort: Medium
- Add audit-visibility integrity warnings
- Warn when local passed-event logging is disabled or delegated audit coverage is missing
- Make reduced visibility explicit in settings, dashboard, and activity surfaces
- Effort: Low to medium
Build the Session Activity Dashboard Widgetβ Done (v3.0.0)This is the smallest meaningful product feature still openIt adds operator value without forcing a major challenge-flow redesignIt will also establish the audit-data persistence layer that other visibility features could reuseEffort: Medium
- Extend Dashboard Widget for Multisite Network Admin (see Β§11.1)
- Build on the v3.0.0 foundation with cross-site aggregation
- Add super admin visibility controls
- This is the natural next step now that Event_Store and the per-site widget exist
- Effort: Medium
- Re-evaluate multisite Network Policy Hierarchy after the dashboard/audit work
- This remains valuable, but only for a narrower audience
- It becomes easier once policy state and audit visibility are clearer
- Effort: High
Use this default order after the v3.4.0 release unless a real user need overrides it:
- Do next: E2E explicit-group runtime monitoring, Plugin Check warning triage, and the next screenshot refresh when UI work changes public screens
- Small release-readiness hardening: localization/translation packaging (POT generation, JS/CLI strings, translator comments, and an i18n check) before the next public packaging push
- Most important major feature track: Gutenberg Block Editor reauthentication β Milestone A (in-editor password modal) shipped (PR #178, 2026-07-07); the live continuation is Milestone B (in-modal 2FA), per
.planning/gutenberg-editor-reauth-milestone-b-2fa-partial-brief.md - Plan next: The Two Factor lifecycle bridge (gate recovery-code generation, TOTP setup/delete, and profile-form provider changes), Patchstack 2FA compatibility as a second-tier bridge/test target, the modest Sudo Activity screen MVP, audit-visibility warnings, multisite operator controls, and governance polish
- Do later if demand exists: Network Policy Hierarchy for Multisite, Cross-Site Session Revocation, network-enforced Passed-event logging policy (super admins can require immutable Passed-event audit visibility across subsites), Security Administrator governance mode (dedicated
manage_wp_sudocapability, settings/widget visibility scoped to that capability, optional strict-mode assignee workflow, and documented recovery path for misconfiguration) - Keep as design backlog: third-party bridge discovery mode, client-side modal challenge, per-session sudo isolation, REST sudo grant endpoint, SSO/SAML/OIDC framework
For the historical v3.1βv3.3 security/governance audit and remediation record, see:
That file is retained for auditability. It is no longer the canonical current
priority tracker; use this roadmap, docs/release-status.md, and
.planning/STATE.md for current planning.
To keep shipping velocity high after v3.4.0, execute the Activity UX slice in this order:
- Widget quick wins (low risk, same data model)
- Add Recent Events header sort toggles (Time/User/Event/Action/Surface) with sensible defaults (Time desc).
- Keep client-side filtering + sorting bounded to the existing 50-row fetch cap to avoid widget-query expansion.
- Preserve the widgetβs current βat-a-glanceβ role; do not add pagination into the widget itself.
- Action-label clarity pass
- Use plain-English primary action labels in UI.
- Keep technical IDs available as secondary metadata (
<code>suffix or tooltip) for operator debugging. - Auto-humanize unknown technical IDs so entries like
options.updaterender as readable text.
- Risk signaling pass
- Add a subtle, accessible
Criticalbadge (text + color, not color-only) for high-risk actions. - Initial badge scope: delete user/plugin/theme, critical settings changes, code/file editor actions, and equivalent network-destructive actions.
- Add a subtle, accessible
- Dedicated Sudo Activity screen (list-table style)
- Add a full Activity admin screen for longer history and analysis beyond the widget.
- Use server-side
WP_List_Tablepatterns: pagination, search, sortable columns, filter controls, and reset filters. - Add CSV export for the current filtered set with capability + nonce checks.
- Keep widget linked to this screen (βView full activity logβ) once the screen ships.
Performance constraints for all four steps
- Keep Event_Store query shape lean (avoid
SELECT *; avoid readingcontext/ipunless explicitly requested). - Keep count/list queries split and indexed for list-table pagination.
- Keep widget-side JS O(n log n) over bounded rows only; do not move full-log workloads into dashboard render.
The integration test suite and manual testing guide run against multiple local environment stacks: nginx + SQLite (Studio), nginx + MySQL (Local), and Apache + MySQL (Local) on macOS with a single PHP version. Apache coverage was added in v2.8.0 via Local by Flywheel sites. Gaps remain in CI and broader hosting diversity.
| Dimension | Current coverage | Gap |
|---|---|---|
| Web server | Apache + MariaDB (wp-env Playwright CI), nginx + php-fpm + MariaDB (stack-smoke CI), nginx + SQLite (Studio local), nginx/Apache + MySQL (Local manual) |
full browser suite still runs only on the default Apache stack |
| PHP version | 8.0β8.4 (unit CI), 8.0/8.1/8.3 (integration CI), 8.2 (Studio/wp-env local) | 8.2 and 8.4 are still missing from integration CI |
| Database | MySQL 8.0 (integration CI), MariaDB LTS (wp-env CI + one integration lane + WP 6.4 / 6.5 compat-sweep lanes), SQLite (Playground stack-smoke CI + Studio local) |
broader MariaDB/version overlap, MySQL 5.7 legacy hosts |
| WordPress version | 6.2 support-floor lane, 6.3β6.6 scheduled compat sweep, 6.7 stable lane, 7.0 forward lane | 6.3β6.6 are not part of required push/PR CI yet |
| OS | macOS (dev), Ubuntu 24.04 (CI) | Windows (if any WP-CLI or path handling is OS-sensitive) |
| Hosting stack | Bare local dev | Shared hosting (cPanel), managed WP (Pressable, WP Engine, Cloudways), containerized (Docker, Kubernetes) |
- Apache
mod_rewritevs nginxtry_files: The challenge page redirect and request replay depend on WordPress rewrite rules. Apache's.htaccessand nginx configs handle these differently. The REST APIAuthorizationheader handling also differs (Apache may strip it unlessCGIPassAuthor.htaccessrules are in place). - PHP version differences:
password_verify()behavior,setcookie()signature changes (PHP 8.0 named params),session_*function availability,json_validate()(8.3+), readonly properties (8.2+). - Database engine: MariaDB and MySQL have subtle JSON and collation differences. The upgrader migration chain and option serialization could behave differently.
- Backward compat: The plugin declares WordPress 6.2+ minimum. CI now includes a dedicated 6.2 floor lane, a scheduled 6.3β6.6 compatibility sweep, plus 6.7 and 7.0 lanes.
Phase A: Expand CI matrix β Done v2.9.2, extended in v2.14.x
CI matrix now covers PHP 8.0β8.4 for unit tests, a 6.2 support-floor integration lane on PHP 8.0, stable/forward integration lanes on PHP 8.1 and 8.3 for WordPress 6.7 and 7.0, one dedicated MariaDB lane, and a scheduled WordPress 6.3β6.6 compatibility sweep on PHP 8.1 with additional WordPress 6.4 and 6.5 MariaDB overlap lanes.
Phase B: Apache + MariaDB CI job β
Covered by Playwright wp-env
The Playwright workflow already runs against the default wp-env Docker stack,
which is Apache + MariaDB. That lane is now named explicitly in CI so it is
visible as an intentional compatibility signal rather than an accidental default.
Phase C: Manual testing matrix (low effort, recurring) β β checklist section added by external PR #98 (merged 2026-06-21); the recurring per-release runs remain.
tests/MANUAL-TESTING.md now has a "Release Environment Matrix Checklist". Before each
release, run the manual guide on at least:
- One Apache environment (DDEV, MAMP, or a staging host)
- One managed WordPress host (Pressable, WP Engine, or Cloudways free trial)
- The minimum supported WordPress version (the floor in
docs/release-status.md; 6.4 as of v4.0.0)
Phase D: Docker-based local testing (medium effort) β partially completed
The repo now has a dedicated nginx + php-fpm + MariaDB Compose stack under
.github/docker/ for CI/browser smoke coverage. A broader contributor-friendly
multi-profile local stack is still future work:
apache-mysql(the classic LAMP stack)nginx-mariadb(alternative)apache-sqlite(WP 6.4+ SQLite support)
This lets any contributor reproduce the full matrix locally.
Phase A (CI matrix expansion) is complete as of v2.9.2. Remaining phases (BβD) are lower priority and should be scoped as a future milestone when Apache/MariaDB testing or managed-host validation becomes a concern.
As of 2026-03-23, the practical testing picture is:
- behavioral coverage is strong in the core challenge flow
- environment coverage is intentionally selective rather than exhaustive
Specifically, the repo now has:
- unit CI on PHP
8.0β8.4 - integration CI on PHP
8.0/8.1/8.3 - automated WordPress lanes for
6.2,6.7, and7.0 - a scheduled WordPress
6.3β6.6compatibility sweep - one MariaDB integration lane in addition to the main MySQL
8.0matrix, plus WordPress6.4and6.5MariaDB overlap lanes in the scheduled sweep - Playwright E2E on Apache + MariaDB via
wp-env - Playwright stack-smoke coverage on explicit nginx + php-fpm + MariaDB
- Playwright stack-smoke coverage on explicit nginx + php-fpm + MariaDB multisite
- Playwright stack-smoke coverage on Playground SQLite
- local/manual SQLite verification documented for Studio in
docs/studio-sqlite-release-runbook.md
Still missing from automation:
- broader MariaDB and legacy-MySQL breadth
- full browser-suite parity on alternate stacks
Recommended next-step test expansion remains intentionally narrow:
- add stack-sensitive smoke cases first, not full-suite parity on alternate stacks
- keep SQLite as release-only assurance plus smoke CI, not a merge gate
- add MariaDB/version overlap gradually instead of turning the compat sweep into a full cartesian matrix
- keep the separate nginx + MariaDB multisite smoke lane narrow: network-admin cancel/return plus one gated replay path before considering any promotion or deeper expansion
That is a good balance for current plugin risk: the dangerous-action challenge and replay flows are well covered, while future matrix expansion should focus on compatibility breadth rather than adding more low-signal tests to already-covered flows.
Status: A single PCOV coverage CI job runs against the unit test suite (PHP 8.3). This establishes a baseline without adding overhead to the full integration matrix.
What's in place:
composer test:coverageβ runs unit tests with PCOV, generatescoverage.xml+ text summary- CI job
unit-tests-coverageβ runs on every push/PR, uploadscoverage.xmlas artifact - No failure threshold yet β the first run establishes the baseline
What's deferred:
- Coverage across the full integration matrix (8 jobs across PHP 8.1/8.3 Γ WP latest/trunk Γ single/multisite). The marginal CI cost is not justified until the matrix is stable.
- Coverage badge. Unit tests mock WordPress functions via Brain\Monkey, so line coverage looks high while entire real code paths (bcrypt, transients, cookies) are untested. A badge communicates accuracy only once the integration suite is comprehensive and the environment matrix is broad.
When to expand: After the environment diversity milestone (Phase A CI matrix expansion). At that point per-matrix-entry coverage adds signal: you can see which combinations of PHP/WP versions hit paths the others miss.
Decision: add mutation testing (Infection PHP) after the environment diversity milestone.
Mutation testing validates that tests actually detect failures by introducing small code changes (mutations) and verifying the test suite catches them. This is the right tool for a security plugin β it directly answers "would our tests catch a regression in the session token comparison or rate limiting logic?"
Why not now:
- Infection re-runs the full test suite for every mutant. With the current suite
(794 unit + 182 integration tests; see
docs/current-metrics.md), a full Infection run would take 10β30 minutes locally. That's acceptable for a pre-release check, not for CI on every push. - The more valuable immediate gap is environment diversity: knowing the tests pass on Apache/MariaDB and WP 6.2β6.9 is higher confidence signal than mutation score on a single stack.
- Mutation testing against mocked unit tests (Brain\Monkey) produces limited signal β mutations in production code are hidden by the mock boundary. Infection is most useful against the integration suite where real code runs.
Recommended approach when the time comes:
- Run Infection against the integration suite only (
--test-framework-options="--config=phpunit-integration.xml.dist"). - Configure a minimum mutation score indicator (MSI) of 80% as a pre-release gate, not a per-push CI gate.
- Focus mutation scope on security-critical classes:
Sudo_Session,Gate,Challengeβ notAdmin,Admin_Bar, orSite_Health. - Add a
composer mutationscript for local runs; keep it out of the standard CI matrix until the integration suite runs fast enough to justify the overhead.
Status: Partially addressed. 9 integration tests in ExitPathTest.php cover the 5 most critical exit paths plus 3 grace window scenarios, using REST dispatch and WPDieException + output capture instead of @runInSeparateProcess. The subprocess approach remains deferred.
The 76 exit/die paths in the codebase (mostly wp_send_json() + exit in the Gate) were the biggest remaining testing blind spot. The 9 tests added in v2.9.2 cover the security-critical shapes:
| Test | Pattern | Verifies |
|---|---|---|
| REST blocked mutation | rest_get_server()->dispatch() |
403, sudo_required error shape |
| AJAX blocked action | WPDieException + ob_get_clean() |
JSON error body, sudo_required code |
| WPGraphQL blocked mutation | check_wpgraphql() + reconstructed JSON |
403, sudo_blocked error shape |
| Admin gating redirect | wp_redirect filter capture |
302 to challenge page with stash_key |
| Challenge wrong password | WPDieException + ob_get_clean() |
JSON error, non-empty message |
| Challenge correct password | WPDieException + ob_get_clean() |
JSON success, authenticated code, session active |
| Grace window admin pass | wp_redirect filter (null) |
No redirect during 120s grace |
| Grace window admin block | wp_redirect filter capture |
Redirect after grace closes |
| Grace window REST pass | rest_get_server()->dispatch() |
No sudo_required during grace |
Remaining: The @runInSeparateProcess approach (real exit() + output capture + header assertions) is still deferred. The WPDieException pattern covers response body shape but cannot verify actual HTTP headers or Set-Cookie output. This matters most for the challenge success path (cookie-setting). Browser-level testing (Playwright) would cover this more naturally than subprocess PHPUnit.
Source: Line-verified code review in .planning/research.md (addendum, lines 567β914). All findings reference actual line numbers in the codebase, verified against composer test:unit, composer lint, and PHPStan. This supersedes the Zen Trinity review in the same file, which contains fabricated code snippets and statistics.
Grace window scope broader than documented intent β
Fixed
Gate permits any matched gated action when the user is within the 120s grace window (class-gate.php:617-619,828-830,960-961).is_within_grace()only checks time window + token validity, not whether the request was in-flight when the session expired (class-sudo-session.php:192-211).docs/security-model.md:153implies grace should only cover in-flight requests, not grant new gated access.Impact: Effective policy is "full gated access for 120 seconds after expiry with valid token" β stronger than documented intent, may weaken audit expectations.Action: Decide on intended semantics. Options: (a) tighten grace to in-flight-only with a stash marker, (b) accept current behavior and update docs to match. Either way, add tests asserting the chosen contract.- Fixed: accepted current behavior (Option B). Updated
security-model.mdandFAQ.mdto document grace as a 120s wind-down window, not in-flight-only. 3 integration tests added (ExitPathTest.phpGRACE-01 through GRACE-03).
MU-plugin makes deactivation non-authoritative β
Fixed
MU shim loads the plugin whenever files exist, regardless ofactive_pluginsoption (mu-plugin/wp-sudo-gate.php:20-24,mu-plugin/wp-sudo-loader.php:22-30).Plugin deactivation callback does not remove the MU shim (class-plugin.php:430-438).FAQ states deactivation returns ungated behavior (FAQ.md:129-132), but that's only true if the shim is absent.Impact: If MU shim is installed, deactivating from Plugins screen doesn't reliably disable runtime behavior.Action: Decide and document intended behavior. If "deactivate means off," remove MU shim on deactivation or add an early bail when plugin is inactive.- Fixed: loader checks
active_plugins/active_sitewide_pluginsbefore loading.uninstall.phpdeletes shim. FAQ updated.
β
Fixedreturn_url double-encoding
Source side pre-encodesreturn_urlbeforeadd_query_arg(class-plugin.php:205-209,class-gate.php:1118-1120,1264-1267,1331-1334).Destination side reads value directly without decoding (class-challenge.php:133-136,191-194).Impact: Cancel/shortcut return behavior can fall back to dashboard instead of the originating page.Action: Pass raw URL toadd_query_arg(let WordPress encode once), or decode before validation on the read path. Add round-trip tests for complex URLs.- Fixed: removed
rawurlencode()from all 4 source locations. Unit test inPluginTest.php.
Site Health stale-session scan capped at 100 users β
Fixed
find_stale_sessions()queriesget_users(... 'number' => 100)(class-site-health.php:256-263).Impact: Large sites can report "no stale sessions" while stale records exist beyond user 100.Action: Paginate through all matching users or maintain a cleanup cursor.- Fixed: paginated
do/whileloop infind_stale_sessions(). Unit test inSiteHealthTest.php.
REST cookie-auth detection only checks β
FixedX-WP-Nonce header
Cookie-auth classifier in Gate checks only forX-WP-Nonceheader (class-gate.php:833-835).No fallback check for_wpnoncerequest param.Impact: Some legitimate cookie-auth clients may be misclassified as headless and routed to app-password policy.Action: Add conservative fallback detection for_wpnoncerequest params. Add tests for mixed cookie-auth request shapes.- Fixed: fallback checks
$_REQUEST['_wpnonce'], matching WP core'srest_cookie_check_errors(). Verified against WP trunk source. 2 unit tests inGateTest.php.
Version constant drift between runtime and test/static bootstrap β
Fixed
Runtime:2.9.1(wp-sudo.php:6,:25). Bootstraps:2.8.0(phpstan-bootstrap.php:13,tests/bootstrap.php:18).Action: Synchronize in release process. Consider extracting version to a single source read by all bootstraps.- Fixed: both bootstraps updated to
2.9.1. Add to release checklist.
2FA default window documentation mismatch β
Fixed
Actual default: 5 minutes (class-sudo-session.php:370). Admin help text: 10 minutes (class-admin.php:323).Action: Align help text to actual default.- Fixed: help text now reads "5 minutes". Note: this is the 2FA authentication window (how long to enter a 2FA code), not the sudo session duration (15 min default). Two distinct timers.
2FA window bounds not enforced in code β
Fixed
FAQ claims 1β15 minute bounds (FAQ.md:139). Code trusts filter return without clamping (class-sudo-session.php:370-371).Action: Clamp filter result to documented min/max, or remove hard-bound language from docs.- Fixed: clamped to 60β900 seconds (1β15 minutes) after
apply_filters. 3 unit tests inSudoSessionTest.php.developer-reference.mdupdated.
Admin bar countdown stalls at β
Fixed0:01 before reload (cosmetic)
When the session expires, the JS countdown decrementsrto0, callswindow.location.reload(), and returns β but the label was last set toSudo: 0:01in the previous tick and is never updated to0:00. The reload latency (1β3s network round-trip) means the timer visually freezes at0:01before disappearing.- Fixed: label now updates to
Sudo: 0:00, interval is cleared, and expiry is announced via SR live region beforereload()fires.
REST error notice has no visible background in WP 7.0 (cosmetic) β
Fixed
- Fixed by adding
#application-passwords-section .notice.notice-errorselector towp-sudo-notices.css. Scoped to the Application Passwords section on profile.php β doesn't affect other admin notices.
Reauthentication flow design: password-first is correct
- The challenge page uses a two-step flow: password entry β optional 2FA (TOTP, email OTP, WebAuthn, etc.). This was evaluated against alternatives (2FA replacing password, method-picker with all factors on one screen, standalone WebAuthn button alongside password).
- Design decision (2026-02-28): The current password-first flow is the right design.
- Reauthentication β login. At reauth, the user already proved identity at login. Any single strong factor suffices to confirm "you're still here." In principle, 2FA could replace the password rather than supplement it. In practice, the tradeoffs make password-first the better default.
- OS-level autofill mitigates password friction. On macOS (Touch ID), Windows (Hello), iOS (Face ID), and Android (biometric), the browser offers biometric autofill for
autocomplete="current-password"fields. Users tap a fingerprint sensor, the password fills, they click confirm. This makes the password step ~2 seconds with no typing β eliminating the main UX argument for a custom WebAuthn button. - WebAuthn as a standalone primary factor is redundant. A "Use passkey" button alongside the password field would save one click over biometric autofill. The engineering cost (custom WebAuthn ceremony UI, bypassing the Two Factor provider's
authentication_page(), new validation paths) is not justified by that marginal gain. - TOTP as a standalone primary factor is the best alternative but has hook-architecture costs. The bridge plugin hooks (
wp_sudo_requires_two_factor,wp_sudo_render_two_factor_fields,wp_sudo_validate_two_factor) all assume the sequential model. Changing to "password OR TOTP" would require new hook semantics and bridge plugin rewrites. - Email OTP as standalone is problematic. Without a preceding password step, anyone who knows the username can trigger OTP email sends β a minor spam/enumeration vector.
- Backup codes as standalone are weak. Shorter than passwords, often stored in plaintext, intended as a fallback β not suitable as a primary reauth factor.
- WebAuthn ceremony UX remains rough when WebAuthn is the active 2FA provider (the
navigator.credentials.get()popup appears after form submission rather than as a natural step). This is a Two Factor plugin provider UX issue, not a WP Sudo architecture issue. The challenge page is provider-agnostic and renders whatever the active provider outputs. - Modal challenge caveat: This analysis assumes the current full-page challenge. If the client-side modal challenge (see Β§10) is implemented, OS-level autofill may not work reliably β browsers are increasingly cautious about autofilling inside iframes and dynamically injected forms, and password manager heuristics depend on page-level cues (form action, URL) that a modal disrupts. If the modal breaks the OS autofill shortcut, an explicit "Use passkey" or "Use TOTP" button on the modal becomes more valuable. Re-evaluate this decision when the modal ships.
- Status: No code changes needed for the current full-page challenge. The flow is optimal given OS-level credential management. Revisit when (a) WordPress core introduces a native reauthentication API that supports method selection, or (b) the modal challenge design begins implementation.
Request stash stores raw POST payloads β
Fixed (Phase 1)
Stash stores verbatim request arrays (class-request-stash.php:65-67,205-212).- Fixed: stash writes now redact sensitive keys and enforce a per-user stash cap before transient storage.
- See
.planning/phases/01-request-stash-redaction-and-upload-action-coverage/.
Progressive delay uses blocking β
Fixed (Phase 2)sleep()
sleep($delay)during failed auth attempts (class-sudo-session.php:718).- Fixed: non-blocking throttle uses
_wp_sudo_throttle_untilplus append-row_wp_sudo_failure_eventtracking; no worker-blocking sleep. - See
.planning/phases/02-non-blocking-rate-limiting/.
App-password admin JS has hardcoded English strings β
Fixed
Hardcoded UI strings inadmin/js/wp-sudo-app-passwords.js(lines 31, 142, 195).Action: Move towp_localize_script()for localization.- Fixed: 3 strings localized via
wp_localize_script()i18n object. Unit test inAdminTest.php.
| Finding | Status |
|---|---|
| Uninstall path has no tests | β
Fixed v2.9.1 β tests/Integration/UninstallTest.php (2 tests) |
| Multisite uninstall network-active branch can under-clean | β
Tested β UninstallTest::test_multisite_uninstall_cleans_user_meta() covers the cleanup path |
| Version constant drift (bootstraps at 2.8.0 vs runtime 2.9.1) | β
Fixed β phpstan-bootstrap.php and tests/bootstrap.php updated to 2.9.1 |
| 2FA default window help text says 10 min, code is 5 min | β
Fixed β class-admin.php:323 now reads "5 minutes" |
| Grace window scope broader than documented | β
Fixed v2.9.2 β docs corrected (security-model.md, FAQ.md), 3 integration tests |
| MU-plugin makes deactivation non-authoritative | β
Fixed v2.9.2 β loader checks active_plugins, uninstall.php deletes shim, FAQ updated |
return_url double-encoding |
β
Fixed v2.9.2 β rawurlencode() removed from 4 locations, unit test added |
| Site Health stale-session scan capped at 100 users | β
Fixed v2.9.2 β paginated do/while loop, unit test added |
| 2FA window bounds not enforced in code | β Fixed v2.9.2 β clamped to 60β900 s, 3 unit tests added |
| App-password JS hardcoded English strings | β
Fixed v2.9.2 β localized via wp_localize_script(), unit test added |
| Exit path testing not started | β
Partially addressed v2.9.2 β 9 integration tests in ExitPathTest.php |
REST cookie-auth _wpnonce fallback |
β
Fixed v2.9.2 β Gate checks $_REQUEST['_wpnonce'] fallback (mirrors WP core), 2 unit tests |
| Admin settings CRUD integration tests | β
Done v2.9.2 β 8 integration tests in AdminTest.php |
| CI matrix missing PHP 8.0 and older WP versions | β Done v2.9.2 β PHP 8.0β8.4, WP 6.7 + latest + trunk |
February 26, 2026
The following areas from our initial design planning and input from others are fully implemented in WP Sudo (through v2.5.2):
- All five threat model scenarios (XSSβRCE, session theft, device compromise, device loss, undetected persistence)
- Post-POST interception with request stash and replay
- Multi-surface coverage with per-surface policy (three-tier model exceeds the document's binary treatment)
- Cryptographic token binding (cookie + SHA-256 in user meta)
- Rate limiting and progressive lockout
- Two Factor plugin integration with browser-bound challenge cookies
- GET request gating (theme switch, network site operations, data export)
- Network-wide multisite sessions and 8 multisite-specific rules
- Per-application-password policy overrides
- 10 audit hooks for external logging
- Proactive session-only authentication (no pending action required)
unfiltered_htmlcapability tamper detection- WPGraphQL surface gating β three-tier policy for GraphQL mutations (Disabled / Limited / Unrestricted), mutation detection heuristic, headless authentication boundary documented (v2.5.0βv2.5.2)
Shipped in v2.12.0
| Feature | Rationale | Effort |
|---|---|---|
wp sudo subcommands |
wp sudo status, wp sudo revoke [--user=<id>], wp sudo revoke --all implemented for operator workflows. |
Medium |
wp_sudo_check() / wp_sudo_require() API |
Third-party plugins can now require sudo without registering full Gate rules. | Medium |
Optional bridge bridges/wp-sudo-stream-bridge.php maps 10 WP Sudo audit hooks into Stream records. |
Low |
These are high-value but architecturally complex. Each needs its own design phase before implementation.
Client-side modal challenge
The ideal UX: .needs-sudo CSS class on forms, JS intercepts
submit, inline password prompt, original form submitted with sudo token. Preserves
form state, handles AJAX saves, matches GitHub/Silverstripe. An
iframe + postMessage architecture would support extensibility with 2FA/SSO providers. Major UX improvement
over redirect-to-challenge, but significant complexity: nonce handling, file uploads
($_FILES not stashable in current model), modal-in-modal for the plugin file editor,
fallback server-side flow. Likely a milestone unto itself.
Per-session sudo isolation
Current model: one sudo token per user in user meta. The ideal is
"different devices, different sessions, different sudo mode state." Integration
with WP_Session_Tokens would provide per-browser isolation β Device A's sudo
would not affect Device B. Architecturally significant; also interacts with nonce
validity if the session layer changes.
REST API sudo grant endpoint
A POST /wp/v2/sudo endpoint for headless clients to enter sudo mode by providing
credentials. Currently headless clients can only be blocked or allowed by policy.
Threat model needs careful thought β the endpoint must require the credential itself,
not just a valid session, because XSS can obtain both rest_nonce and the auth cookie.
SSO / SAML / OIDC provider framework
SSO protocols support IsPassive=true (SAML) and prompt=none (OIDC) as
silent reauthentication mechanisms. Currently there is no formal registration or
dispatch for SSO providers in the challenge flow. Would need a provider interface
(register, render, validate) parallel to the existing 2FA hooks.
| Idea | Reason discarded |
|---|---|
| IP binding as default | Too many false positives (mobile, IPv6 rotation, proxies, CDNs). Acceptable as an opt-in constant/filter, wrong as a default. |
sudo-{$cap} capability wrapper |
Backward-incompatible on older WP versions, conflates authentication state with role capabilities. |
| Process-scoped tokens (UAC style) | No session persistence per HTTP request. The session model is correct for web. |
| Assumed roles / role-switching | Facilitating Editor-by-default β assume-Admin is a different product concern, closer to User Switching. Out of scope. |
| Require sudo to access Network Admin | Low value relative to complexity. Gate specific destructive actions instead. |
- Section 1 (Integration Tests): New features should have integration tests from
the start β "login grants sudo" and "grace period" are both testable against real
bcrypt and
wp_loginhooks. - Section 2 (WP 7.0 Prep): The Abilities API (section 2) and the REST sudo grant endpoint (above) are complementary β both expand WP Sudo's coverage of non-browser surfaces. Monitor together.
- Section 5 (Environment Diversity): The modal challenge (if implemented) is the feature most sensitive to environment differences β JavaScript, nonce handling, and cookie behavior vary across caching layers, reverse proxies, and hosting stacks.
Items carried forward from the pre-v2.4 roadmap. Features completed in v2.0.0βv2.3.1
(Site Health integration, progressive rate limiting, CSP-compatible assets, lockout
countdown, admin notice fallback, gated actions table, v2 architecture, editor
unfiltered_html restriction, per-app-password policies, PHPStan level 6, CycloneDX
SBOM, accessibility roadmap) are documented in the CHANGELOG.
WP Activity Log (WSAL) Sensor Extension β shipped v2.11.0 as bridges/wp-sudo-wsal-sensor.php. It maps 11 audit hooks to WSAL events (IDs 1900001β1900011); the audit-hook total has since grown (see docs/current-metrics.md), so newer hooks such as the 4.1.0 wp_sudo_escalation_blocked are not yet mapped (tracked under Escalation guard follow-ups). Inert when WSAL absent.
Stream bridge β implemented on main for v2.12.0 as bridges/wp-sudo-stream-bridge.php. Optional mu-plugin mapping for those 11 audit hooks (same coverage caveat as the WSAL bridge).
WP-CLI β implemented on wp sudo commandsmain for v2.12.0 (status, revoke --user, revoke --all).
Public β implemented on wp_sudo_check() / wp_sudo_require() APImain for v2.12.0 for third-party action gating integrations.
Multi-Dimensional Rate Limiting (IP + User) β shipped v2.13.0. Per-IP tracking via transients alongside per-user tracking, combined lockout policy, and the triggering IP address added as the third wp_sudo_lockout hook argument.
Password-manager / 2FA interaction with reauth (research). How password managers
(1Password, Bitwarden, iCloud Keychain, browser built-ins, Dashlane) interact with WP
Sudo's reauth surfaces β the full-page wp-sudo-challenge form and, if the Tier 2
block-editor feature ships, an in-editor modal β is unverified. Managers key off
login-form heuristics (autocomplete tokens, field semantics, a real <form>), and
our custom challenge handler is deliberately not wp-login.php; a wp.components.Modal
field is the most autofill-hostile surface. 1Password is reportedly/anecdotally harder
with two-factor lately (unverified β needs a concrete version + repro before it is
treated as fact). Also covers how WordPress/two-factor renders each provider (TOTP,
WebAuthn/passkey, backup codes) at our challenge time. Deliverable: a per-manager Γ
per-surface autofill matrix plus any cheap markup fixes. Tracked in
.planning/todos/pending/2026-07-05-password-manager-2fa-interaction.md; feeds the
Tier 2 modal autofill check in .planning/gutenberg-editor-reauth-phase2-plan.md.
Orphaned-user sweep (optional, opt-in) β When the admin-escalation guard
(wp_sudo_guard_escalation, opt-in / default OFF) blocks a one-shot administrator
creation, the user row is inserted before the role is applied, so the blocked
request can leave a roleless, powerless user record (never an admin). Per the
escalation-guard analysis Β§11 "Orphan-cleanup decision (Option A)"
(admin-escalation-guard-analysis.md), we deliberately do not delete that row
in-hook β mid-request wp_delete_user() on a possibly-unauthenticated request
carries real risks (admin-file loading, multisite wpmu_delete_user, deletion /
post-reassignment hooks firing during an attack). Backlog: design an optional,
opt-in sweep (scheduled or shutdown-time) that reclaims never-completed roleless
users left by a blocked escalation β scoped to rows provably created-and-left-roleless
in a blocked request, reversible, and off by default. The guard shipped in 4.1.0
(opt-in, default OFF), so this sweep is relevant whenever the guard is enabled.
Escalation-guard blind spots (future mitigation β tracked, not yet scoped).
The 4.1.0 admin-escalation guard hooks the {prefix}capabilities meta write and
grant_super_admin, so by construction it does not see three escalation
shapes. Tracked here so they are not forgotten:
- Runtime capability grants via
user_has_cap/map_meta_capfilters. A plugin can confer administrator-equivalent capabilities per-request without ever writing role/capabilities meta. A meta-write guard cannot observe this; a capability-based check would catch custom admin-equivalent roles but carries a high false-positive cost (it must distinguish legitimate per-request grants). Needs a design before any attempt. - Direct
$wpdbwrites to the usermeta table. Raw SQL that sets the capabilities row bypasses the metadata filters entirely. Anything with direct DB access has broader reach already, so this is low-priority; a periodic capabilities-integrity audit (compare stored caps against a known-good baseline) is the only plausible mitigation and is out of scope for now. - In-session residual window. An escalation that fires during a legitimate admin's own active sudo window (e.g. CSRF or stored XSS landing mid-session) is allowed, because the actor holds a valid session. Mitigations would be orthogonal (per-action re-prompt for the highest-risk grants, CSRF hardening) and trade UX for coverage; tracked, not scheduled.
Tentative: MU-plugin role/capability lockdown mode (research backlog).
Explore whether the optional MU-plugin should support a high-assurance mode that
loads an operator-reviewed file manifest of trusted privileged principals and
denies or repairs database-stored role/capability drift at runtime. This would be
aimed at database-only compromise or accidental role-editor drift, where an
attacker can mutate wp_user_roles, {prefix}capabilities, or multisite
site_admins but cannot write PHP/files. It should be treated as an
audit-first / enforce-later hardening feature, not a default behavior:
- start with administrator, super-admin, and WP Sudo governance-cap holders;
- explicitly account for per-user capabilities, role definitions, and multisite
super admins rather than only locking
wp_user_roles; - provide a safe snapshot/generate workflow plus filesystem-only break-glass;
- document blind spots: filesystem compromise, runtime grants via
user_has_cap/map_meta_cap, SSO/directory sync, role-management plugins, and legitimate provisioning changes.
Do not scope implementation until research answers whether enforcement should
filter effective capabilities, rewrite stored state, or only alert; how false
positives are avoided on sites with dynamic roles; and how recovery works if the
manifest is stale. Research tracker:
.planning/todos/pending/2026-07-15-mu-role-cap-lockdown-mode.md.
Bridge coverage is also incomplete: the WSAL/Stream bridges do not yet map the
4.1.0 wp_sudo_escalation_blocked event β adding it would let SIEM/audit tools
alert on escalation blocks directly. See the bridge-coverage backlog below.
Two Factor lifecycle bridge
Add an optional bridge, guarded by class_exists( 'Two_Factor_Core' ), that gates
security-sensitive factor lifecycle operations in the upstream Two Factor plugin.
This is a concrete gap in WP Sudo's current coverage: a user can have no active
WP Sudo session while Two Factor's own session/revalidation logic still permits
factor management. If a browser session is compromised and the attacker also
knows or phishes the password, newly generated recovery codes can satisfy WP
Sudo's later 2FA step.
Verified against WordPress/two-factor commit
38cd183
on 2026-04-29:
- Recovery-code generation is
POST /two-factor/1.0/generate-backup-codes, registered inproviders/class-two-factor-backup-codes.php. The callback generates plaintext codes, stores hashed codes in_two_factor_backup_codes, and returns plaintextcodesin the REST response (same file). - TOTP lifecycle uses
POSTandDELETEon/two-factor/1.0/totp, registered inproviders/class-two-factor-totp.php. Setup writes_two_factor_totp_keyand can enable the provider; deletion disables the provider and deletes the key (same file). - Profile-form updates save
_two_factor_enabled_providersand_two_factor_providerduringpersonal_options_update/edit_user_profile_update; Two Factor's owncurrent_user_can_update_two_factor_options()uses its revalidation window, not WP Sudo's session state (class-two-factor-core.php,class-two-factor-core.php).
Recommended bridge rules:
auth.two_factor_backup_codes.generate- REST: route
#^/two-factor/1\.0/generate-backup-codes$#, methodPOST - Label: Generate Two Factor recovery codes
- REST: route
auth.two_factor_totp.manage- REST: route
#^/two-factor/1\.0/totp$#, methodsPOST,DELETE - Label: Configure Two Factor authenticator app
- REST: route
auth.two_factor_profile.update- Admin:
profile.php/user-edit.php,action=update, methodPOST - Callback: gate only when Two Factor form fields are present, such as
_nonce_user_two_factor_options,_two_factor_enabled_providers, or_two_factor_provider - Label: Change Two Factor settings
- Admin:
Implementation notes:
- Ship as a bridge first, parallel to the existing WebAuthn bridge, rather than expanding core rules unconditionally.
- Keep the rule labels focused on factor lifecycle, not ordinary profile edits.
- Add unit coverage for rule injection and matching; add integration/manual tests for recovery-code generation without sudo, TOTP setup/delete, and profile-form provider changes.
- Document that Two Factor's own revalidation is still useful, but WP Sudo should require its own active sudo session before creating or replacing a factor that can satisfy future sudo challenges.
Impact: High for session-compromise defense. This closes a concrete bridge gap where factor-management actions can create credentials used to pass later sudo 2FA checks.
Patchstack 2FA compatibility target
Patchstack Security includes its own paid-feature TOTP flow rather than using the
upstream WordPress/two-factor provider API. Confirmed first-hand against
Patchstack Pro 2.3.6 source (file last-changed SVN revision 3433693, 2026-01-06):
the login integration is enabled by the patchstack_login_2fa option, stores
per-user state in webarx_2fa_enabled, webarx_2fa_secretkey, and
webarx_2fa_secretkey_nonce, collects the login code from patchstack_2fa, and
validates with TokenAuth6238::verify() in
includes/login.php.
2FA registration is limited to licensed Pro installs (the constructor returns
early in free-license mode). The core bridge path is now runtime-validated
offline against a legitimately licensed Pro 2.3.6 fixture; the live login-form
challenge, profile.php save, and WooCommerce lifecycle remain manual-test. See
docs/two-factor-ecosystem.md β Patchstack for the
full evidence.
Recommended scope:
- Track Patchstack as a second-tier 2FA compatibility target, behind the upstream Two Factor lifecycle bridge.
- Remaining manual compatibility testing: exercise the live login-form
challenge, the
profile.phpsave flow, and the WooCommerce account-form lifecycle (the core detection/validation path is already runtime-validated offline). - If demand justifies a bridge, detect enrollment via the public
webarx_2fa_enableduser option and validate through Patchstack's ownTokenAuth6238::verify(). Verification needs the plaintext secret, which Patchstack exposes only through the privateP_Login::tfa_get_secret(), so a bridge must reach it via reflection or replicate the libsodium decrypt ofwebarx_2fa_secretkey/_nonceβ validating "without reading the secret" is not possible for this plugin's design. - Treat Patchstack profile 2FA enable/disable as a factor-lifecycle action that should be considered for gating in the same design pass as other 2FA lifecycle bridges.
Impact: Medium. This broadens compatibility coverage for a security-plugin
audience, but is less urgent than the upstream Two Factor lifecycle bridge. The
core detection/validation path is now runtime-validated offline against a
legitimately licensed Pro 2.3.6 fixture; the remaining work is the live
login-form challenge, profile.php save, and WooCommerce account-form lifecycle
runs, plus the decision on shipping a dedicated bridge vs. keeping Patchstack
behind the upstream Two Factor lifecycle bridge.
Session Activity Dashboard Widget
Admin dashboard widget showing active sudo sessions (count + user list), recent gated operations (last 24 h from audit hooks), and policy summary. On multisite, a network admin widget could show activity across all sites.
Requires storing audit data β currently the hooks fire-and-forget with no persistence. A lightweight custom table or transient-based ring buffer would be needed.
Impact: Medium β useful visibility for site administrators, but not a security improvement.
Recommended MVP slice
Start with the smallest operator-visible version:
- Active sudo sessions
- current count
- top 5 users with active sessions
- expiry time remaining for each visible session
- Recent gated operations
- last 10 gated or replayed actions
- action label, user, site (on multisite), timestamp, outcome
- Current policy summary
- session duration
- entry-point policy summary (
REST,CLI,Cron,XML-RPC,WPGraphQL)
Do not start with:
- long-term analytics
- charts
- per-IP forensics
- custom filtering UI
- full historical reporting
The MVP goal is fast operational visibility, not a reporting subsystem.
Recommended data model
The preferred first implementation is a small custom audit table rather than a network of transients:
- table name:
{$wpdb->base_prefix}wpsudo_events - columns:
idsite_iduser_ideventrule_idsurfaceobject_typeobject_idipcreated_atcontext(longtextJSON for small optional metadata)
Why a table first:
- recent-activity widgets are query-shaped, not cache-shaped
- multisite summaries are much easier from one table than from per-site transient shards
- retention and pruning are straightforward
- future dashboard, export, and CLI/reporting features can share the same source
Recommended write scope for the first pass:
- persist only the small subset of events the widget needs:
wp_sudo_action_gatedwp_sudo_action_replayedwp_sudo_action_allowedwp_sudo_action_blockedwp_sudo_lockout
Recommended retention for the MVP:
- prune rows older than 7 or 14 days via opportunistic cleanup or a lightweight scheduled task
This keeps the schema useful without turning WP Sudo into a full audit-log product.
Third-Party Bridge Discovery Mode (report-only)
Opt-in scanner/assistant that helps operators and maintainers find third-party plugin mutation surfaces that WP Sudo does not know about yet. This is a bridge-coverage feature, not an enforcement engine: it should produce evidence, confidence levels, and candidate rules for human review.
Recommended MVP:
- Static entry-point scan
- detect
wp_ajax_*,wp_ajax_nopriv_*,admin_post_*, andadmin_post_nopriv_*registrations - classify unauthenticated
*_nopriv_*surfaces as advisory/hard-deny candidates, not sudo-challenge candidates - detect
register_rest_route()registrations, including namespace/route/method hints when statically readable - flag generic dispatchers on
admin_init,init,template_redirect,parse_request, andrest_api_init
- detect
- Dangerous sink scan
- flag callbacks/files that call user, role/capability, critical-option, application-password, plugin/theme, filesystem, or direct
$wpdbwrite APIs - distinguish high-confidence direct calls from low-confidence generic dispatchers
- flag callbacks/files that call user, role/capability, critical-option, application-password, plugin/theme, filesystem, or direct
- Bridge-rule suggestions
- generate disabled-by-default candidate snippets for
wp_sudo_gated_actionswhen the surface is authenticated and challengeable - include plugin/file/line evidence, suspected surface, suspected sink, and confidence
- never auto-enable generated rules without operator or maintainer review
- generate disabled-by-default candidate snippets for
- Update/activation review signal
- after plugin activation or update, optionally report newly discovered mutation surfaces
- prefer admin notice + report link over automatic blocking
Non-goals for the MVP:
- no per-hook allow/deny matrix
- no continuous
all-hook runtime tracking on every request - no vulnerability/reputation scanner
- no automatic enforcement from static-analysis guesses
- no claim that every custom dangerous action can be detected
Impact: Medium to high for third-party bridge coverage and operator awareness. It directly addresses the known limitation that WP Sudo gates known request shapes and core consequence hooks, while custom plugin actions may be named differently. Keep the implementation report-only so it adds coverage intelligence without turning WP Sudo into a generic plugin firewall.
Gutenberg Block Editor Integration
Detect block editor context and queue the reauthentication requirement instead of
interrupting save. Show a snackbar-style notice using the @wordpress/notices
API. Expected to require extracting challenge rendering from class-challenge.php
into a reusable component. The snackbar flow needs a different UI surface but the
same auth verification and stash-replay machinery. This is also the natural moment
to add Playwright E2E tests covering both the existing challenge page and the new
editor integration.
Impact: Medium β improves UX for block editor users, but the current stash-replay pattern already works for most editor operations.
Why this stays behind the dashboard widget
This is not just a UI polish item. It requires decisions about:
- how a save interruption maps to a snackbar or modal flow
- how request stash/replay works when the editor owns the request lifecycle
- how 2FA provider rendering works outside the current full-page challenge
- how file uploads, nonces, and nested dialogs behave in the editor UI
- how the browser test matrix expands once challenge transport is no longer page-based
- transport choice: an
apiFetchmiddleware that catches thesudo_requiredREST error, opens an in-editor challenge, then re-dispatches the original request (client-side retry) β likely cleaner than server-side stash/replay for REST-driven editor saves. Weigh client-retry vs. reusing the existing stash/replay machinery. - surface inventory (do first): enumerate which editor flows actually hit the gate β most post saves are not gated; it's specific gated options/operations reachable from editor panels. Scope the real trigger set before designing UI.
- build tooling: the plugin currently has no build step; an editor bundle
introduces one (e.g.
@wordpress/scripts). That is a standalone architecture decision, not a detail.
So the recommended order is:
- ship the dashboard widget first
- use that work to establish durable audit persistence
- then start a dedicated design phase for block-editor/modal challenge transport before writing production code
Network Policy Hierarchy for Multisite
Super admins set minimum session duration and maximum allowed entry-point policies
at the network level. Site admins can only tighten (not loosen) these constraints.
Expected to require extracting a Settings or Policy class from class-admin.php.
The current direct get_site_option() access would need to merge network-level
floors with per-site overrides and enforce the "can only tighten" constraint.
Impact: Medium β valuable for large multisite networks with delegated site administration. Not relevant for single-site installs.
These ideas are inspired by REST-surface management tooling, but only the parts that strengthen WP Sudo's own operator experience belong here. The guiding rule is: improve visibility, testing, and emergency control without weakening the role-agnostic reauthentication model.
Priority Lockdown Presets β MVP shipped on main (unreleased)
One-click emergency presets for remote or non-interactive surfaces are now implemented in the settings UI for:
REST App PasswordsXML-RPCWP-CLICronWPGraphQL
The shipped MVP uses a small set of opinionated presets, not a policy wizard:
- Normal β current recommended defaults
- Incident Lockdown β tighten all remote surfaces to the most restrictive safe policy
- Headless Friendly β preserve intentional API surfaces while tightening legacy or rarely used ones
Current implementation
- Adds a preset layer above the existing per-surface settings in
class-admin.php - Stores a
policy_presetmarker inwp_sudo_settings, while concrete per-surface values remain the source of truth after application - Requires explicit apply confirmation on save before a preset overwrites the per-surface controls
- Fires a dedicated
wp_sudo_policy_preset_appliedaudit hook so logging plugins can record preset application - Includes unit, integration, Stream/WSAL bridge, and browser happy-path coverage for preset application
- Does not add per-user, per-role, or per-IP bypasses
Impact: High during incident response and support. Low implementation risk because it reuses the existing policy model.
Follow-on improvements
- Consider a richer diff/confirmation experience only if operators ask for more than the current explicit apply checkbox + summary notice.
- Evaluate whether future Site Health output should surface the active preset marker.
- Keep avoiding silent changes to current recommended defaults, underspecified preset semantics, and duplicated policy logic.
Request / Rule Tester β MVP shipped on main (unreleased)
An internal diagnostic panel now appears on Settings β Sudo for evaluating representative request shapes without executing them.
Current implementation
- Adds a Request / Rule Tester panel to the existing settings screen
- Supports MVP request simulation for
admin,ajax, andrest - Accepts method, URL, authenticated/active-sudo toggles, network-admin toggle,
and REST auth mode (
cookie,application_password,bearer,none) - Reuses
Gatematching logic through a side-effect-freeevaluate_diagnostic_request()helper instead of reimplementing rule parsing in JavaScript - Returns matched rule information, decision (
allow,gate,soft-block,hard-block), stash/replay eligibility, and explanatory notes - Includes unit coverage for the pure evaluator and settings-page rendering
Follow-on improvements
- Decide whether the network-admin toggle should simulate more of the multisite runtime context than the current MVP does
- Add deeper request/body simulation only if real operator use cases justify the complexity
- Consider future support for
cli,cron,xmlrpc, orwpgraphqlonce a reliable diagnostic model exists for those surfaces
Impact: High for maintainers, site operators, and third-party integrators trying to understand why a request was gated.
Protection Status Panel
An at-a-glance operator panel summarizing whether WP Sudo is in a healthy, fully-protective state.
Recommended first-pass indicators:
- MU-plugin installed or not
- active sudo session / grace window state for the current user
- 2FA integration detected or not
- current surface policy summary
- recent lockouts or recently gated actions
- whether the current environment is using HTTPS
Implementation details
- Prefer a compact settings-page panel or dashboard widget summary, not a new top-level admin screen
- Reuse existing Site Health checks where possible instead of duplicating diagnostics logic
- Pull recent activity from the planned
wpsudo_eventstable when available; until then, show only live configuration/state - Keep the first version read-only; no inline policy editing from the status panel
Impact: High for day-to-day operability and support triage.
Expanded Diagnostics / Site Health Coverage
Broaden WP Sudo's existing diagnostics so operators can quickly confirm that the plugin is enforcing what they think it is enforcing.
Suggested additions:
- challenge URL reachability
- cookie security flags / HTTPS posture
- REST and AJAX fail-closed sanity checks
- detected 2FA bridge/plugin state
- multisite network-policy visibility
Implementation details
- Extend
Site_Healthrather than building a parallel diagnostics subsystem - Separate hard failures from advisory warnings
- Make each diagnostic link back to the exact setting or doc section that explains remediation
- Keep destructive checks simulated where possible; use passive introspection before active probes
Impact: Medium-high. Helps support and release verification without changing enforcement behavior.
Clearer Inline Help / Operator Guidance
Surface more of the existing docs directly inside the settings UI so admins understand the consequences of each policy without leaving the page.
Implementation details
- Add concise help text beside each surface policy and sensitive setting
- Reuse the existing help-tab pattern instead of inventing a new docs UI
- Link to
security-model.md,developer-reference.md,two-factor-integration.md, and the new authentication deep references where relevant - Prioritize βwhy this mattersβ guidance over long feature descriptions
Impact: Medium. Reduces operator mistakes and support load.
Custom REST / AJAX Sudo Error Semantics
Allow careful customization of the error message and possibly the response code used when WP Sudo denies a request for lack of an active sudo session.
Implementation details
- Prefer filter-based customization first; do not start with a broad admin UI
- Preserve stable machine-readable identifiers such as
sudo_required - Do not make the default more permissive or ambiguous for clients
- Restrict any response-code customization to safe alternatives and document the interoperability risks
Impact: Medium for advanced integrators, low for typical site owners.
HTTPS / TLS Posture Warnings
Warn when WP Sudo is operating in conditions that weaken the security story for challenge and session cookies.
Implementation details
- Start with warnings only; do not hard-block or silently rewrite site behavior
- Detect non-HTTPS admin usage, insecure cookie context, or proxy/header misconfiguration where practical
- Surface warnings in Site Health and the settings status panel
- Link to remediation docs instead of trying to auto-fix server configuration
Impact: Medium. Valuable security guidance, but should stay advisory unless a future design phase justifies stricter behavior.
To keep WP Sudo aligned with its core model, this backlog does not include:
- per-role REST route matrices
- user or IP whitelisting for sudo bypass
- arbitrary REST data exposure features
- broad API-management behavior unrelated to reauthentication or fail-closed enforcement
SBOM Enhancements
The CycloneDX SBOM (.sbom/bom.json) currently reflects only the PHP/Composer dependency
graph (zero production dependencies). Options:
- GitHub Action for CI-generated SBOMs on every release tag.
- JS dependency tracking if Gutenberg integration introduces an npm build step.
- Whole-site SBOM tooling references in security documentation.
JS Testing with Playwright
No JS tests exist today. The vanilla JS files have no build step and limited surface area, so the cost-benefit of Jest + JSDOM mocks is low. The natural trigger is Gutenberg integration, which would require browser-level testing anyway.
| Feature | Reason |
|---|---|
| Session extension (extend without reauth) | Undermines the time-bounded trust model and violates zero-trust principles. The keyboard shortcut (Cmd+Shift+S / Ctrl+Shift+S) makes re-authentication fast enough. |
| Passkey/WebAuthn as a standalone reauthentication method | Evaluated and declined (2026-02-28). OS-level biometric autofill (Touch ID, Windows Hello, Face ID) already provides a smooth passwordless-like UX for the password field β a custom WebAuthn button saves one click at significant engineering cost. TOTP-only reauth is the strongest alternative but requires bridge hook redesign. Email OTP standalone has enumeration risk; backup codes standalone are too weak. The password-first + optional 2FA flow is correct for reauthentication. See Β§9 reauthentication flow design. WebAuthn key registration/deletion gating is a separate concern, addressed by the bridge plugin (bridges/wp-sudo-webauthn-bridge.php, shipped v2.10.0). |
Added April 19, 2026
For multisite networks, WP Sudo's current dashboard widget operates at the per-site level only. Super admins managing large networks need cross-site visibility and control. This section outlines the multisite-specific feature roadmap.
Problem: The current Dashboard_Widget (includes/class-dashboard-widget.php) shows active sessions and recent events for the current site only. Super admins in the network admin dashboard have no aggregated view of sudo activity across all sites.
Proposed solution: Add a network-level dashboard widget at wp-admin/network/index.php for super admins only.
Network widget sections:
-
Network-wide active sessions
- Total count of active sudo sessions across all sites
- Top 5 users with active sessions (any site)
- Per-user breakdown showing which sites they have active sessions on
- Click-through to individual site dashboards
-
Network event aggregation
- Last 10β20 events from
wpsudo_eventstable across all sites - Site column showing which site each event occurred on
- Filter by event type (lockout, gated, blocked, allowed, replayed)
- Optional date range filter
- Last 10β20 events from
-
Network policy overview
- Summary of which sites diverge from the network default policy (if Network Policy Hierarchy is implemented)
- Count of sites using each preset (Normal, Incident Lockdown, Headless Friendly)
- Warning indicators for sites with Unrestricted policies on sensitive surfaces
Implementation details:
- Register widget on
wp_network_dashboard_setuphook - Require
manage_network_optionscapability - Query
wpsudo_eventstable withoutsite_idfilter for cross-site aggregation - Active sessions query: iterate sites with
get_sites()or use a network-wide user meta query - Consider pagination or lazy-load for networks with many sites (100+)
- Reuse
Dashboard_Widgetrendering patterns where possible
Effort: Medium. The data layer (Event_Store) already supports cross-site queries. Main work is the network dashboard hook integration and UI expansion.
Problem: Site admins (users with manage_options but not manage_network) see the per-site dashboard widget. Some super admins may want to:
- Hide the widget from site admins entirely (security through obscurity, or to reduce UI clutter)
- Restrict visibility to super admins only across the network
- Allow site admins to see their own site's activity but not network-aggregated data
Proposed solution: Add network-level settings for widget visibility.
Setting options:
| Setting | Behavior |
|---|---|
| Default (current) | Widget visible to any user with manage_options on their site |
| Super Admins only | Widget hidden from site admins; only super admins see it (even on per-site dashboards) |
| Network Dashboard only | Per-site widget hidden from all users; only the network dashboard widget is available (super admins only) |
| Disabled | No dashboard widgets anywhere |
Implementation details:
- Store setting in
wp_sitemetaaswp_sudo_network_widget_visibility - Check setting in
Dashboard_Widget::register()before adding widget - For "Super Admins only" mode:
current_user_can( 'manage_network' )instead ofmanage_options - Network settings page: add a Widget Visibility section under Network Settings β Sudo (if network settings page exists) or as a field on the network admin settings screen
- Graceful fallback: if setting is missing, use current behavior (visible to
manage_options)
Effort: Low. This is a capability check and network option, not a new data layer.
Problem: Super admins may need to revoke sudo sessions network-wide during an incident (e.g., compromised super admin account, coordinated attack).
Proposed solution: Add network-level revocation controls.
Features:
- Revoke All Network Sessions β One-click button in network dashboard widget or network settings to expire all active sudo sessions across all sites
- Revoke by User β Search for a user and revoke their sessions on all sites where they have one
- Revoke by Site β Select a site and revoke all sessions on that site
Implementation details:
- Reuse
Sudo_Session::deactivate()per-user - For network-wide revocation: iterate all users with
_wp_sudo_expiresmeta > now - Consider batching for large networks to avoid timeout
- Fire
wp_sudo_deactivatedhook for each revocation (audit trail) - Add
wp_sudo_network_session_revokedaction for network-level logging
Effort: Medium. The revocation logic exists; this is UI + network iteration.
The existing Network Policy Hierarchy concept (Β§11 Feature Backlog) defines super admin policy floors. This section adds complementary multisite controls.
Additional controls:
- Network-level preset enforcement β Super admin can lock all sites to a specific preset (e.g., "Incident Lockdown" network-wide during an active threat)
- Per-site policy overrides visibility β Network settings page shows which sites have custom policies diverging from the network default
- Policy audit log β Record when site admins change policies, visible in network dashboard widget
Implementation details:
- Store network preset lock in
wp_sitemeta - On site settings load: if network lock is active, disable per-site controls and show notice
- Policy change logging: extend
Event_Recorderto capturewp_sudo_settingsoption updates
Effort: High. This touches the settings architecture and adds a new audit event type.
- Network Dashboard Widget β Highest value, reuses existing data layer
- Super Admin Widget Visibility Controls β Low effort, addresses immediate operator need
- Cross-Site Session Revocation β Medium effort, valuable for incident response
- Network Policy Hierarchy Enhancements β High effort, defer until base multisite tooling is stable
To avoid scope creep, this multisite expansion does not include:
- Per-site customization of the dashboard widget layout
- Real-time activity streaming / WebSocket push
- Site grouping or tagging for partial network views
- Integration with third-party network management plugins
- Cross-network federation (multiple WordPress networks)
Added April 20, 2026. Revised April 20, 2026 to reflect clean-launch framing (no public install base to migrate).
WP Sudo's threat model already includes compromised admin sessions, but current
control surfaces are still tied to broad admin capabilities (manage_options
on single-site and manage_network_options on multisite). This section
tracks the hardening plan to make Sudo governance explicit and least-privilege.
As of docs/release-status.md, the plugin has not yet shipped to the WordPress.org directory; submission is intentionally delayed/on hold while readiness is maintained. There is no
existing install base to protect from a capability-model transition, so the
rollout is compressed from the original three-phase compatibility-first plan:
strict-capability mode ships as the default in v3.1. Compatibility mode exists
as an opt-in (for single-admin smoke testing and CI fixtures), but is not a
migration endpoint.
-
Phase 1 (v3.1, shipped) β ship strict governance as the default.
- Introduce the full capability surface (
manage_wp_sudo,view_wp_sudo_activity,export_wp_sudo_activity,revoke_wp_sudo_sessions) with a centralizedsudo_can()helper. - Strict mode is the default; compatibility mode available via
wp_sudo_governance_modeoption. - Add the
options.wp_sudo_accessgated rule so grants, revokes, and session revocations are all subject to the reauth challenge. - Add Access tab with drift detection, "last manager" guard, and audit
hooks (
wp_sudo_capability_granted,wp_sudo_capability_revoked,wp_sudo_session_revoked). - Add
WP_SUDO_RECOVERY_MODEbreak-glass constant per the spec's Break-glass section. - Replace all
current_user_can('manage_options')checks in governance surfaces withsudo_can(). Exit criterion: zero direct checks remain.
- Introduce the full capability surface (
-
Phase 2 (optional future polish) β governance polish.
- Integrity warnings when effective visibility is broader than intended.
- Opt-in 2FA-enrollment requirement for
manage_wp_sudoholders. - Audit visibility on governance-mode transitions.
-
Phase 3 (post-v3.4 follow-up) β access tab UX.
-
User picker instead of user ID field. The Grant Capability interface currently requires a numeric user ID. Replace with a searchable dropdown (or typeahead) populated from real WordPress users. Default scope: administrator-role users only, since only admins can hold Sudo governance capabilities in practice β there is no current use case for granting
manage_wp_sudoto an editor or subscriber who would be unable to reach the settings page anyway. Leave the question open in case a future role model warrants wider eligibility (e.g., a custom role withmanage_optionsthat should be able to receiveview_wp_sudo_activity). -
Human-readable capability labels in the dropdown. The grant interface currently presents raw capability strings (
manage_wp_sudo,view_wp_sudo_activity, etc.). Replace with plain English descriptions and demote the capability name to secondary label text:Capability slug Suggested display label manage_wp_sudoManage Sudo settings and policies view_wp_sudo_activityView sudo activity and sessions export_wp_sudo_activityExport sudo activity data revoke_wp_sudo_sessionsRevoke other users' active sessions The capability slug should remain visible (as subtitle or tooltip) so developers and integrators can identify capabilities unambiguously without relying on display strings.
-
- This is a security-governance control, not a cosmetic UX feature.
- It reduces insider-risk and accidental policy drift in organizations with many admins.
- It complements (not replaces) external logging/audit systems.
- Shipping strict from v3.1 is practical specifically because there is no install base to migrate; deferring to a multi-version guided migration would buy no safety and add real complexity.
Added March 4, 2026 β based on independent assessments by Codex, Gemini, and Claude. Full analysis in .planning/PROPOSED-NEXT-STEPS-{codex,gemini,Claude}.md and .planning/WORKING-ASSESSMENT-Codex.md.
WP Sudo ran a focused hardening sprint before new UX or architecture expansion. Sprint A and Sprint B are complete (request stash redaction/cap, upload-action coverage, and non-blocking rate limiting). Remaining work should prioritize reliability hardening and observability over lower-impact feature expansion.
Added June 7, 2026 β based on a deep manual security review of main at
67008e4.
The review found no obvious unauthenticated RCE, SQL injection, stored XSS, CSRF, open redirect, or plain privilege escalation in the core paths reviewed. It did identify two code-level weaknesses and two product-security hardening items that should shape the next phases.
Finding: 2FA failure lockout can be reset before 2FA succeeds. β Shipped in v3.2.0.
- Failure counters now reset only after final sudo activation succeeds.
- Repeated
password -> bad 2FA -> passwordcycles continue accumulating failures and reach lockout. - No-2FA password success still clears prior failed attempts after activation.
Finding: WPGraphQL Limited mode can fail open on encoded or persisted mutations.
- The fallback classifier searches the raw POST body for the literal substring
mutation. - JSON-escaped operation text such as
\u006dutationcan decode to a GraphQL mutation while bypassing the raw-body fallback. - Persisted query/APQ requests can also omit the operation text unless a site
supplies a classifier through
wp_sudo_wpgraphql_classification.
Fix (shipped in v3.2.0):
- Decode JSON bodies, GET/form
queryparams, and multipartoperationsGraphQL request payloads before fallback classification. - Handle batched GraphQL payloads conservatively: any decoded mutation should classify the request as a mutation.
- Fail closed on unknown persisted operations unless a classifier identifies the operation as a query.
- Update
docs/security-model.mdso the heuristic caveat names encoded payloads, multipart operations, batches, and persisted operations precisely.
Tests:
- Unit tests for inline mutation, JSON-escaped
mutation, query false-positive strings, batched mutation/query mixtures, GETqueryparams, multipartoperations, classifier override precedence, and unknown persisted operation handling. - Integration coverage for WPGraphQL Limited mode once a representative request fixture can be exercised.
Finding (reconciled 2026-06-21): request-stash redaction is exact-match + suffix-based, not exact-key only.
Request_Stash::is_sensitive_key()(includes/class-request-stash.php,@since 2.11.0) matches each lowercased field name against an exact list (sensitive_field_keys()) AND 28 secret-value suffixes (SENSITIVE_KEY_SUFFIXES) such as_api_key,-secret,accesstoken, andapisecret, viastr_ends_with.- Residual: third-party camelCase names whose lowercased form ends in no listed suffix β
e.g.
clientSecret,refreshToken,authorization,bearer,totp_seedβ are still not redacted. The suffix list deliberately omits baresecret/tokento avoid false positives. Broadening this is intentionally out of scope (REQUIREMENTS); the fix sketch below is retained as a future option, not an open gap.
Fix:
- Add conservative pattern-based redaction for common secret-like substrings and suffixes.
- Add custom-rule metadata for fields that must be redacted, fields that are safe to replay, and actions that should not replay POST bodies after reauth.
- Fail explicitly when a redacted required field makes replay unsafe.
Tests:
- Unit tests for camelCase, snake_case, dashed, nested, and array secret keys.
- Replay tests proving built-in safe forms still work and unsafe custom forms fail with an operator-readable message.
Finding: Sudo governance still relies on broad administrator capabilities.
- This is documented as an internal governance boundary, not a bug, but it limits WP Sudo's effectiveness for organizations with many administrators.
- A user who can satisfy sudo and manage options can weaken WP Sudo's policies, remove optional hardening, or widen headless surfaces.
Fix:
- Continue the dedicated capability plan in Β§11.2 before adding lower-impact
features, and evolve governance with dedicated
manage_wp_sudo-style capabilities, stronger policy-change protections, and safer custom-rule replay/redaction controls. - Require explicit
manage_wp_sudo-style authority for settings, policy changes, activity export, and session revocation. - Gate policy weakening and governance-mode transitions as high-risk actions.
- Keep a documented recovery/break-glass path so least-privilege controls do not create unrecoverable lockouts.
- Audit product/docs language for the authentication/authorization boundary, starting with the admin Help dropdown copy and public FAQ/readme language. Preferred framing: "Sudo verifies that the current user is still the account holder; WordPress still decides whether that user is allowed to perform the action." Avoid wording that implies WordPress role/capability checks happen only after a post-auth replay.
Tests:
- Capability matrix tests for single-site and multisite.
- Tests proving ordinary administrators without Sudo management authority cannot relax policies, view restricted activity, export logs, or revoke sessions.
- Break-glass tests that prove recovery mode is explicit, auditable, and bounded.
Finding: WP_SUDO_RECOVERY_MODE break-glass is uncontained. (Found June 13, 2026; β
contained June 13, 2026 β items 1, 3, 4 below shipped in v3.4.0. Item 2, the scoped single-user form, remains.)
Verified against the code path and WordPress core admin-page routing (describes the pre-fix behavior):
- The recovery branch in
wp_sudo_can()and the cap mapping inwp_sudo_map_governance_meta_cap()(includes/functions-governance.php) have no role gate. WhileWP_SUDO_RECOVERY_MODEis defined truthy, any logged-in user checking their ownmanage_wp_sudoreceives it β Subscribers and Editors included, single-site and per-site multisite users alike. (Multisite super admins already short-circuit earlier and are unaffected.) manage_wp_sudois the master governance capability: a holder can open the Access tab and self-grant the other three caps and change gating policy. So the open surface is "every authenticated account β full Sudo governance" for as long as the constant is set.- WordPress core gates the Sudo submenu page by its own capability
(
user_can_access_admin_page()βcurrent_user_can('manage_wp_sudo'), mapped toexistin recovery mode), so a non-admin reachingoptions-general.php?page=wp-sudo-settingsdirectly passes β the hidden Settings menu is not a real barrier. - The promised safeguards never existed: no admin notice and no
governance.recovery_modeaudit event are implemented anywhere inincludes/.docs/security-model.md,docs/FAQ.md, andreadme.mdpreviously asserted them (corrected docs-only; seedocs/llm-lies-log.md#20).
This violates Phase R3's own acceptance criterion that recovery mode be "explicit,
auditable, and bounded." The practical risk is operator error rather than remote
escalation (defining the constant requires wp-config.php write access), but a
break-glass mechanism should minimize the blast radius of its open window.
Fix (the intended features):
- β
Role-gate the grant (primary). Shipped. In both
wp_sudo_can()andwp_sudo_map_governance_meta_cap(), the recovery branch now also requiresmanage_options(single-site) /manage_network_options(multisite). A legitimate locked-out admin still passes while non-admins no longer gain control. The multisite super-admin short-circuit is intact. - Optional scoped form. Remaining follow-up. Support
define('WP_SUDO_RECOVERY_MODE', <user_id_or_login>)to grant only that user;trueretains the (now admin-only) broad behavior. Tightest containment for operators who want surgical recovery β and the proper fix for the non-admin Sudo manager that item 1 deliberately does not rescue. - β
Surface a non-dismissible admin notice on the Sudo settings screen
whenever recovery mode is active. Shipped (
Admin::render_recovery_mode_notice()). - β
Emit a
wp_sudo_recovery_mode_activeaudit hook (stored as a sampledrecovery_modeevent). Shipped.docs/current-metrics.mdaudit-hook count anddocs/developer-reference.mdupdated. The stored event is unprefixedrecovery_mode, not the archivedgovernance.recovery_modeliteral (that spec never shipped; consistency with existing Event_Store names wins). - Visibility / monitoring follow-up. Remaining β design reviewed June 13, 2026.
The notice (item 3) and audit event (item 4) both fire only inside
Admin::render_settings_page(), so an operator who enables recovery, regains access, and navigates away is never reminded again, and an external SIEM never learns the bypass is active unless someone opens the Sudo settings page.- Ship: a
Site_Health::test_recovery_mode()returning a critical status keyed strictly onwp_sudo_is_recovery_mode()β pull-based, REST-exposed (so monitoring polls it), reflects live state, no cron dependence. Register it inSite_Health::register_tests()alongsidetest_stale_sessions; confirm it surfaces to the responsible operator on multisite (recovery maps tomanage_network_options). - Dropped: a daily-cron audit heartbeat. It is unreliable exactly on the
neglected sites where recovery is most likely left on (opportunistic WP-Cron /
DISABLE_WP_CRON), is redundant with the Site Health REST status, and would force auser_id = 0row that corrupts both the per-user sampling bucket and the documented "user accessing the Sudo surface" semantics ofwp_sudo_recovery_mode_active. If a recorded daily signal is ever required, it must be a dedicatedrecovery_mode_heartbeathook + event type, never a reuse ofrecovery_mode_active. - Deferred: broadening the notice to
all_admin_notices. Notice fatigue plus multisite capability nuance outweigh the marginal reminder value once the Site Health flag exists. - Naming / disambiguation. WP Sudo's "recovery mode" shares the phrase with
WordPress core's own recovery mode (
WP_Recovery_Mode, the fatal-error handler; e.g. the core cron eventrecovery_mode_clean_expired_keys), which confuses readers β a tester mistook a core Site Health cron warning for a WP Sudo fault. Decision: do not rename the public API. Every WP Sudo symbol is already prefixed (WP_SUDO_RECOVERY_MODE,wp_sudo_is_recovery_mode(),wp_sudo_recovery_mode_active), so there is no code collision β only the English phrase overlaps β and renaming would break operatorwp-config.phpconstants, external SIEM hook subscribers, and the storedrecovery_modeevent type (and its historical rows). Instead: (a) standardize "break-glass" as the lead term in all human-facing text (notice, FAQ, security-model, the future Site Health test description) so "recovery" rarely appears unqualified; (b) one safe, non-breaking relabel β the dashboard event label "Recovery" (a display string, not a stored value or API symbol) β "Break-glass"; (c) a one-line FAQ/security-model note disambiguating from core'sWP_Recovery_Mode. Any true symbol rename is deferred to a major version with deprecation aliases.
- Ship: a
Process: Shipped via a Pre-Implementation Design Review (reviewer agent + design
brief), strict TDD, and the Pre-Commit Reviewer Workflow, per CLAUDE.md.
Tests:
wp_sudo_can()/wp_sudo_map_governance_meta_cap(): recovery mode grantsmanage_wp_sudoto an admin (hasmanage_options) but not to a non-admin (Subscriber/Editor); other three caps are never granted by recovery mode; behavior is per-current-user only.- Multisite: super-admin short-circuit still passes; per-site non-admin denied;
per-site admin (
manage_network_options) passes. - Scoped-constant form (if built): only the named user/ID is granted; others denied.
- Notice renders on Sudo admin pages while active and is absent when the constant is removed (no DB persistence).
- Audit event fires once per recovery-mode admin page load (or per access grant β decide in design review) with the expected payload.
- Site Health (item 5):
test_recovery_mode()returnscriticalonly when the constant is set; returnsgood/absent in both strict and compatibility governance mode (no false positives); surfaces on multisite where the network operator sees it.
Docs reconciled (shipped):
- β
Restored the (now accurate) notice/event/role-gate descriptions in
docs/security-model.md,docs/FAQ.md, andreadme.md. - β
Updated
docs/llm-lies-log.md#20 notes to mark the containment as built. - β
tests/MANUAL-TESTING.mdΒ§23.8 documents the contained behavior β it expects denial for non-administrators and includes steps for the non-dismissible notice and the sampledrecovery_modeaudit event.
Goal: shorten feedback loops without weakening the release-grade browser coverage that protects challenge, replay, admin UI, and WordPress 7.0 behavior.
Completed first slice:
- Shard the default Chromium Playwright suite across four GitHub runners.
- Preserve a final aggregate check named
E2E Testsso branch protection can keep relying on the existing required status. - Upload per-run artifacts and cancel stale same-branch E2E runs after new pushes.
Completed second slice:
- Replace opaque Playwright
--shardassignment with four explicit CI groups:challenge-basic-admin,challenge-2fa-ui,challenge-lockout-surfaces, andchallenge-replay-multisite. - Split the heavy
challenge.spec.tscoverage by behavior using--grepranges, without moving or rewriting the test bodies. - Keep the aggregate
E2E Testsrequired status check unchanged.
Next implementation options, in priority order:
- Measure the explicit-group runtime on GitHub Actions. If a new long pole appears, adjust group membership before adding caching or custom images.
- Cache Playwright browser binaries (
~/.cache/ms-playwright) with keys derived frompackage-lock.jsonand the pinned Playwright version. Keep Linux system dependency installation explicit unless a runner image proves it is stable. - Evaluate a prebuilt CI image containing Node, Playwright system dependencies,
Chromium, Docker tooling, and the
wp-envcompatibility patch. Adopt only if image maintenance is cheaper than repeated runner bootstrap time. - Split smoke and full E2E policy: keep fast smoke coverage required on ordinary PRs, keep the full suite required for release-grade deployments, admin/browser UX changes, request gating/replay changes, and manual dispatches.
- Add conservative path-based skipping only for low-risk docs, metadata, dependency-lockfile, or Blueprint-only changes. Use a required placeholder check rather than silently omitting required status checks, and never skip full E2E for security, request gating, replay, wp-env/Playwright infrastructure, or release-candidate changes.
- Audit test isolation before enabling Playwright workers inside a single
WordPress environment. Do not raise
workersabove1until shared database, cookie, user, nonce, and option state are proven isolated or reset per test. - Keep
retries: 2until grouped CI establishes a lower flake baseline; reducing retries is a later optimization, not the first stability lever.
Acceptance criteria:
- Each optimization must preserve a visible required status for branch protection.
- Full E2E must remain easy to trigger manually before tags and release branches.
- Low-risk skips must be documented in the E2E Validation Policy and must fail closed when changed paths are ambiguous.
- Record explicit-group distribution and runtime after rebalancing; no group should own all challenge lockout, 2FA, and replay coverage.
- Measure before/after wall-clock duration for
E2E Testsand record material changes indocs/release-status.mdor the release notes when they affect release process expectations.
Status: completed on main after v3.4.0. The refreshed public screenshots
use the WordPress Playground demo on WordPress 7.0, default admin color scheme,
1280x760 CSS viewport cropped to the WordPress frame, 2x device scale, and the
seeded demo users/sessions/events from blueprint-main.json plus the recovery
state from blueprint-recovery-mode.json. Future UI-changing phases should add
their own screenshot-refresh task when public docs would otherwise drift.
Goal: bring public screenshots back in line with the v3.2 UI and make the README/readme assets show the screens operators now use.
Scope:
- Inventory current README,
readme.txt, WordPress.org asset, and docs image references before deciding whether to patch or fully replace screenshots. - Capture a consistent screenshot set for Settings -> Sudo, the Access tab, Session Activity dashboard widget, Rule Tester, Help dropdown where useful, and any other new screens or widgets added since the last visual refresh.
- Prefer redoing the full set when old screenshots use stale admin styling, inconsistent viewport sizes, or no longer explain the current product surface.
- Document the capture environment, viewport, admin color scheme, WordPress version, and demo data assumptions so future screenshot updates are repeatable.
- Check that no passwords, nonces, session tokens, real user emails, API keys, or private site URLs appear in screenshots.
Acceptance criteria:
- README/readme screenshots match current UI and cover the new screens/widgets.
- Images are optimized, accessible with useful alt text/captions, and suitable for both GitHub README and WordPress.org plugin asset expectations.
- Visual QA confirms the screenshots are readable on common desktop widths and do not contradict current Playground/demo behavior.
Goal: make the plugin cleanly translation-ready before the next public packaging push. Current PHP UI coverage is mostly good, but the repository does not yet ship a generated POT catalog and a few operator/JavaScript strings still need explicit localization.
Scope:
- Add a
languages/wp-sudo.potgeneration workflow, preferably viawp i18n make-pot, and document the command. - Add a Composer script such as
composer i18n:potso release prep can refresh the POT without remembering the raw command. - Localize admin-bar countdown JavaScript strings through PHP-provided script
data, or move them to
@wordpress/i18nif the JavaScript build/runtime grows enough to justify that dependency. - Localize WP-CLI operator messages, including plural forms with
_n()where counts are displayed. - Add translator comments for placeholder-heavy strings and any security text where word order matters.
- Add a lightweight CI or release-prep check that detects stale/generated POT output, or explicitly documents POT regeneration as a required pre-release manual step.
Acceptance criteria:
- User-facing PHP, JavaScript, and WP-CLI strings are either translatable or deliberately excluded because they are technical identifiers, selectors, or developer-only diagnostics.
- The generated POT file includes current plugin strings and can be refreshed reproducibly from a documented command.
composer lintremains clean, including WordPress text-domain and translator comment sniffs.- This phase remains release-readiness hardening, not a security blocker, unless the plugin is being packaged for WordPress.org or another translator-facing distribution channel.
- Follow TDD: failing tests first, then minimal implementation, then refactor.
composer test:unit,composer lint, andcomposer analysepass locally.- Integration tests are required for cross-class auth flows and REST/WPGraphQL surface behavior.
- Full Playwright E2E is required only if the implementation changes browser/admin UX, request replay, challenge flow, or release-grade deployment confidence.
Problem: Request_Stash::sanitize_params() (class-request-stash.php:205) returns $_POST data verbatim, including passwords and tokens. These are stored in WordPress transients (wp_options table) accessible to any code with database read access, backup systems, and object cache backends.
Fix:
- Implement secret-key redaction before transient storage (recursive, case-insensitive matching on
password,pass,user_pass,token,secret,api_key, etc.). - Add filter for extending/overriding the sensitive key list.
- Add per-user stash cap (e.g., 5 concurrent) with oldest-first eviction to bound growth from authenticated abuse.
- Preserve replay fidelity for non-secret fields; fail safely with explicit error if a redacted field is required for replay.
Tests: Unit tests for redaction and allowlist behavior, stash cap eviction. Integration tests confirming built-in replay flows still work.
Problem: Action_Registry gates install-plugin and install-theme (WordPress.org directory installs) but has no rules for update.php?action=upload-plugin or upload-theme (ZIP upload paths). A compromised session can upload arbitrary plugin ZIPs without sudo challenge.
Fix: Add explicit action mappings for upload-plugin and upload-theme in the Action Registry.
Tests: Unit tests for matching upload action requests. Integration tests confirming challenge path on upload actions.
Problem: Sudo_Session::record_failed_attempt() (class-sudo-session.php:719) uses sleep() for progressive delays (2s at attempt 4, 5s at attempt 5). Under concurrent abuse, this blocks PHP-FPM workers and reduces site throughput. The read-modify-write integer counter also has a TOCTOU race window.
Fix: Replace sleep() with non-blocking time-based throttling. Use a model that narrows race susceptibility without blocking workers. Preserve existing hook contracts (wp_sudo_reauth_failed, wp_sudo_lockout).
Tests: Unit tests for threshold and lockout windows. Integration tests for repeated failures and lockout expiration.
Problem: Action_Registry::get_rules() returns the output of wp_sudo_gated_actions filter without schema validation. Malformed rules from third-party code could cause silent matching failures. (Note: safe_preg_match() already guards against regex crashes, so this is a reliability issue, not a crash risk.)
Fix (shipped): Added filtered-rule normalization/validation in Action_Registry::get_rules(). Invalid rules are dropped fail-closed per rule (required scalar metadata + array-or-null surfaces), and non-array filter payloads fall back to built-in rules. Filter contract and cache behavior are preserved.
Tests: Unit coverage for valid/invalid/mixed filtered rule sets and built-in matching resilience with malformed custom rules. Integration coverage added for mixed valid/invalid custom rules, malformed surface shapes, and non-array payload fallback behavior.
Problem: mu-plugin/wp-sudo-loader.php:22 hardcodes 'wp-sudo/wp-sudo.php' as the plugin basename. The loader fails silently in non-standard directory layouts.
Fix (shipped): Hardened basename/path detection in mu-plugin/wp-sudo-loader.php using configured basename, loader-derived basename, and canonical fallback strategies. Added explicit unresolved-path diagnostic action (wp_sudo_mu_loader_unresolved_plugin_path). Canonical installs remain unchanged.
Tests: Unit coverage for basename fallback construction, non-canonical active-slug recognition, inert behavior when inactive, and unresolved-path diagnostics.
Related (shipped): MU-plugin install UX now detects is_writable() on the mu-plugins directory. When the directory is not writable (common on managed hosts), the Install button is hidden and manual copy instructions are shown expanded instead.
Problem: Mutation detection in Limited mode (class-gate.php:919) uses str_contains($body, 'mutation'). Persisted queries send only a query ID, not the operation text, so mutations sent via the Persisted Queries extension bypass detection.
Fix (shipped): Added classifier hook wp_sudo_wpgraphql_classification so persisted-query environments can explicitly classify request bodies as mutation/query. Unknown classifier output falls back to legacy heuristic behavior.
Tests: Unit coverage for classifier mutation/query/unknown fallback behavior and bypass precedence. Integration assertions added for persisted-query mutation block and query pass-through flows.
Problem: Audit hooks exist but have no integration with enterprise logging tools.
Fix (shipped): Added optional bridge bridges/wp-sudo-wsal-sensor.php mapping WP Sudo audit hooks into structured WSAL events. The bridge remains inert when WSAL APIs are unavailable. Stream parity also shipped later via bridges/wp-sudo-stream-bridge.php; see docs/current-metrics.md / docs/developer-reference.md for the current audit-hook total.
Tests: Unit tests for WSAL presence/absence behavior, listener registration, payload mapping, and pass-through hook contract safety.
| Feature | Reason |
|---|---|
| Modal challenge rewrite | Design-heavy; no security improvement over current stash-challenge-replay flow. |
| Per-session/device sudo isolation | Valuable but requires larger architectural change; not a hardening item. |
enforce_editor_unfiltered_html() optimization |
Negligible overhead (cached role lookup); relocating to admin_init introduces a detection gap on frontend requests. |
| REST early-exit optimization | No measured bottleneck at 29 rules; worth revisiting if rule count grows significantly. |
Status: Complete. Initial audit items resolved in v2.2.0βv2.3.1. Follow-up audit items (v2.4.0βv2.10.0 UI additions) resolved in v2.10.1.
All Critical, High, Medium, and Low severity items from the WCAG 2.1 AA audit and WCAG 2.2 AA follow-up audit have been addressed:
- Escape key guard (WCAG 3.2.2):
aria-liveannouncement with 600 ms delay before navigating away from the challenge page. - Step-change announcement (WCAG 4.1.3): Password β 2FA transition announced
via
wp.a11y.speak(). - Settings label-input association (WCAG 1.3.1): All
add_settings_field()calls includelabel_formatching the rendered inputid. - Replay status message (WCAG 4.1.3): Visible "Replaying your action..." message
and
wp.a11y.speak()announcement before form submission. - Localized JavaScript strings (i18n): All user-facing strings passed through
wp_localize_script(). - Session expiry handling (WCAG 2.2.1): "Start over" button replaces automatic reload.
- Reduced motion preferences:
@media (prefers-reduced-motion: reduce)rules in both CSS files. - Focus-visible outlines:
:focus-visibleoutlines with proper offset. - Gated actions table semantics: Native table semantics with
<caption>element (replacedrole="presentation"in v2.2.0). - Disabled link contrast: Changed to
#787c82(4.6:1 ratio, WCAG AA). - Admin notice ARIA roles:
role="alert"on blocked-action notice,role="status"on gate notice. - MU-plugin message area:
role="status"andaria-live="polite". - Admin bar countdown cleanup:
pagehidelistener clears interval, prevents bfcache issues. - Settings default value documentation (WCAG 3.3.5): Inline
<p class="description">text on all fields. - Lockout countdown SR throttling (WCAG 4.1.3):
aria-live="off"with 30-second and 10-secondannounce()intervals.
Three accessibility gaps found in UI added after v2.3.1:
- Per-app-password policy SR feedback (WCAG 4.1.3): Save success/error was
visual-only (outline color). Added
wp.a11y.speak()announcements for save confirmation and error states. Addedwp-a11yas script dependency. - Disabled action button semantics (WCAG 4.1.2):
aria-disabled="true"on<a>elements (theme/plugin pages) withoutrole="button"β screen readers may not announce disabled state on native links. Addedrole="button"to disabled<a>elements. - MU-plugin message
aria-atomic(WCAG 4.1.3):role="status"+aria-live="polite"message element was missingaria-atomic="true", so content replacements may only announce changed text nodes. Added attribute.
Also fixed in this pass:
- Admin bar countdown 0:01 stall (WCAG 4.1.3): Timer label never updated to
0:00on session expiry β last visible value was0:01during reload latency. Fixed: label updates to0:00, interval cleared, expiry announced before reload.