Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.

Commit 1a899fc

Browse files
authored
docs: align docs with shipped 4.1.0 code and prep release (#113)
Pre-release documentation sweep aligning all docs with the shipped 4.1.0 code (escalation guard #111, REST backstop #104), with code as the source of truth: escalation guard documented (filters/action/constant) with its default-OFF posture and blind spots stated in FAQ + security-model; corrected now-false boundary claims (multisite cap-key regex; role/user-create coverage); analysis doc + gate-completeness report marked implemented/closed; Gate architecture description updated in CLAUDE.md/AGENTS.md; CHANGELOG dated and release-status refreshed for the 4.1.0 line; blind spots logged as tracked future mitigation; drift findings + external-claims audit recorded in llm-lies-log. Docs-only.
1 parent e9da0b8 commit 1a899fc

14 files changed

Lines changed: 196 additions & 119 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ behavior that the model cannot hold in working memory.
232232
### Core Classes (all in `includes/`, namespace `WP_Sudo`)
233233

234234
- **Plugin** — Orchestrator. Creates and owns the component instances. Handles activation/deactivation hooks. Strips `unfiltered_html` from editors on activation and restores it on deactivation. Expires sudo session on password change (`after_password_reset`, `profile_update`).
235-
- **Gate** — Multi-surface interceptor. Matches incoming requests against the Action Registry and gates them via reauthentication (admin UI), error response (AJAX/REST), or policy (CLI/Cron/XML-RPC/App Passwords).
235+
- **Gate** — Multi-surface interceptor. Matches incoming requests against the Action Registry and gates them via reauthentication (admin UI), error response (AJAX/REST), or policy (CLI/Cron/XML-RPC/App Passwords). Beyond request-pattern matching it also arms **effect-level backstops** — interactive (`admin_init`) and REST (`register_rest_backstop`) — that hard-block unambiguous destructive effects reached through non-enumerated handlers (4.1.0), plus an opt-in, **default-OFF admin-escalation guard** (`arm_escalation_guard`, filter `wp_sudo_guard_escalation`) that blocks a newly-granted administrator/super-admin without an active sudo session by hooking the `{prefix}capabilities` meta write and `grant_super_admin`.
236236
- **Action_Registry** — Defines all built-in gated rules and rule categories. Extensible via `wp_sudo_gated_actions` filter. See `docs/current-metrics.md` for the current single-site/multisite totals.
237237
- **Challenge** — Interstitial reauthentication page. Handles password authentication, 2FA integration, request stash/replay.
238238
- **Sudo_Session** — Session management. Cryptographic token (user meta + httponly cookie), rate limiting (5 attempts → 5-min lockout), session binding. Two-tier expiry: `is_active()` for true session state; `is_within_grace()` for the 120 s grace window after expiry (token-verified). Cleanup deferred until grace window closes.

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## 4.1.0 - unreleased
3+
## 4.1.0 - 2026-06-24
44

55
- **Security (gate completeness):** Two coordinated-disclosure findings in the
66
action-gating model are closed. Affected versions: ≤ 4.0.0 (both predate 4.0.0

CLAUDE.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ No build step. No production dependencies — only dev dependencies (PHPUnit 9.6
126126
- `docs/ROADMAP.md` — unified roadmap: integration tests, WP 7.0 prep, collaboration analysis, TDD strategy, core design features, feature backlog, accessibility appendix.
127127
- `docs/release-status.md` — canonical current release status: stable tag, unreleased `main` work, and WordPress forward-lane posture.
128128
- `docs/wporg-submission-checklist.md` — WordPress.org submission process: validation gates, asset specs, SVN trunk/tags layout, and the version-sync gate.
129-
- `docs/documentation-remediation-checklist.md` — audit-driven cleanup checklist for doc drift and archival labeling.
130129

131130
## Verification Requirements
132131

@@ -273,7 +272,7 @@ Specifically, do **not** write:
273272
### Core Classes (all in `includes/`, namespace `WP_Sudo`)
274273

275274
- **Plugin** — Orchestrator. Creates and owns the component instances. Handles activation/deactivation hooks. Strips `unfiltered_html` from editors on activation and restores it on deactivation. Expires sudo session on password change (`after_password_reset`, `profile_update`).
276-
- **Gate** — Multi-surface interceptor. Matches incoming requests against the Action Registry and gates them via reauthentication (admin UI), error response (AJAX/REST), or policy (CLI/Cron/XML-RPC/App Passwords).
275+
- **Gate** — Multi-surface interceptor. Matches incoming requests against the Action Registry and gates them via reauthentication (admin UI), error response (AJAX/REST), or policy (CLI/Cron/XML-RPC/App Passwords). Beyond request-pattern matching it also arms **effect-level backstops** — interactive (`admin_init`) and REST (`register_rest_backstop`) — that hard-block unambiguous destructive effects reached through non-enumerated handlers (4.1.0), plus an opt-in, **default-OFF admin-escalation guard** (`arm_escalation_guard`, filter `wp_sudo_guard_escalation`) that blocks a newly-granted administrator/super-admin without an active sudo session by hooking the `{prefix}capabilities` meta write and `grant_super_admin`.
277276
- **Action_Registry** — Defines all built-in gated rules and rule categories. Extensible via `wp_sudo_gated_actions` filter. See `docs/current-metrics.md` for the current single-site/multisite totals.
278277
- **Challenge** — Interstitial reauthentication page. Handles password authentication, 2FA integration, request stash/replay.
279278
- **Sudo_Session** — Session management. Cryptographic token (user meta + httponly cookie), rate limiting (5 attempts → 5-min lockout), session binding. Two-tier expiry: `is_active()` for true session state; `is_within_grace()` for the 120 s grace window after expiry (token-verified). Cleanup deferred until grace window closes.

docs/FAQ.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ It depends entirely on where the exploit path goes.
3232

3333
**Yes, if the attack crosses a covered surface.** An attacker who exploits a plugin vulnerability to gain a session and then tries to perform a covered high-risk operation — activate a plugin, create an admin user, change a role — through the standard admin UI, AJAX, or REST path that WP Sudo intercepts will hit the gate. The attacker has no sudo session, so the operation is blocked regardless of how they obtained their session. This is the core kill-chain interception: even with a valid session, covered destructive actions require credential proof.
3434

35-
**No, if the exploit runs through the plugin's own path.** If the vulnerable plugin calls `wp_set_role()`, `wp_insert_user()`, or similar WordPress functions from inside its own AJAX handler or REST endpoint — without routing through a standard surface WP Sudo has been positioned to intercept — the gate never fires. WP Sudo cannot invent interception points for code it does not see.
35+
**Yes, even through the plugin's own path — if you enable the admin-escalation guard (4.1.0, opt-in, default OFF).** Turning on the `wp_sudo_guard_escalation` filter makes WP Sudo hook the *effect* of an administrator grant (the `{prefix}capabilities` user-meta write and `grant_super_admin`) rather than a surface, so it blocks a **newly granted** `administrator` (single-site) or super-admin (multisite) on *any* path — a plugin's own AJAX/REST handler, or even an unauthenticated request — whenever the actor has no active sudo session. Because an unauthenticated or low-privilege attacker cannot hold a sudo session, this neutralizes the most common privilege-escalation shape even when the vulnerable plugin's own permission check is broken. It is off by default and intentionally narrow: it fires only for *new* administrator/super-admin grants (never low-privilege roles, demotions, or idempotent re-grants), it defers on CLI/Cron/XML-RPC and on Unrestricted REST Application-Password surfaces, and it **cannot** see administrator capabilities conferred at runtime via the `user_has_cap`/`map_meta_cap` filters or written directly with `$wpdb`. Enable it with `add_filter( 'wp_sudo_guard_escalation', '__return_true' )`; allowlist trusted provisioners with `wp_sudo_allow_escalation` or the `WP_SUDO_ALLOW_ESCALATION` constant.
36+
37+
**Otherwise, no, if the exploit runs through the plugin's own path.** Apart from that opt-in administrator/super-admin guard, if the vulnerable plugin calls `wp_set_role()` (to a non-admin role), `wp_insert_user()`, or similar WordPress functions from inside its own AJAX handler or REST endpoint — without routing through a standard surface WP Sudo has been positioned to intercept — the gate never fires. WP Sudo cannot invent interception points for code it does not see.
3638

3739
**No, if the attack runs inside an already-active sudo session.** If the exploited browser session already has an active sudo window, WP Sudo will generally not re-challenge until that window expires. The outcome at that point depends entirely on the underlying authorization logic in the exploited code. See the next question.
3840

@@ -113,7 +115,7 @@ change live gate state.
113115

114116
Users with the `manage_wp_sudo` capability. On a new install, the activating admin receives all four governance capabilities automatically. Other administrators can be granted access from **Settings → Sudo → Access**.
115117

116-
In compatibility mode (`wp_sudo_governance_mode = compatibility`), access falls back to `manage_options` (or `manage_network_options` on multisite). Compatibility mode is an opt-in fallback — the default is strict mode, which requires `manage_wp_sudo` explicitly.
118+
Governance is always strict since 4.0.0: access requires the `manage_wp_sudo` capability explicitly, with no `manage_options` fallback. (The `compatibility` governance mode and the `wp_sudo_governance_mode` option were removed in 4.0.0.) On multisite, super admins are always treated as holding the governance capabilities via the `is_super_admin()` short-circuit in `wp_sudo_can()`.
117119

118120
## What are the four governance capabilities?
119121

docs/ROADMAP.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ The operator tooling tranche shipped in v2.12.0.
197197
- ~~Action Registry schema validation~~`normalize_filtered_rules()` validates and normalizes `wp_sudo_gated_actions` filter output; malformed rules dropped fail-closed.
198198
- ~~MU loader resilience~~ — basename/path resolution uses explicit fallback chain (`WP_SUDO_PLUGIN_BASENAME` → derived → canonical); diagnostic action on unresolved paths.
199199
- ~~WPGraphQL persisted-query strategy~~`wp_sudo_wpgraphql_classification` filter enables external mutation classification for persisted-query setups; `str_contains` heuristic preserved as fallback.
200-
- ~~WSAL sensor bridge~~`bridges/wp-sudo-wsal-sensor.php` now maps all 11 audit hooks to structured WSAL events (IDs 1900001–1900011); inert when WSAL absent.
200+
- ~~WSAL sensor bridge~~`bridges/wp-sudo-wsal-sensor.php` maps 11 audit hooks to structured WSAL events (IDs 1900001–1900011); inert when WSAL absent. (The audit-hook total has since grown — see `docs/current-metrics.md`; newer hooks like `wp_sudo_escalation_blocked` are not yet bridged.)
201201

202202
### ✓ Completed in v2.10.0
203203

@@ -549,7 +549,7 @@ not context retrieval.
549549
With the WordPress 7.0 compatibility release and v3.4.0 hardening work closed,
550550
the recommended implementation order is:
551551

552-
1. **Deprecate `compatibility` governance mode** (break-glass containment now ✅ shipped) — Two paired governance security-debt items. (a) *Still open:* fire `_doing_it_wrong()` + persistent admin notice when `wp_sudo_governance_mode = 'compatibility'` is active; update FAQ and developer reference; queue removal for the next major release. (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 the `wp_sudo_recovery_mode_active` audit hook fires (stored as a sampled `recovery_mode` event), so it is explicit, auditable, and bounded (see §12.1 Phase R3). `WP_SUDO_RECOVERY_MODE` is 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)
552+
1. **Deprecate `compatibility` governance mode** (✅ resolved) — Two paired governance security-debt items. (a) *Resolved in v4.0.0:* rather than a deprecation cycle, the `compatibility` mode and the `wp_sudo_governance_mode` option 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 a `wp_sudo_inert_governance_mode_detected` audit 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 the `wp_sudo_recovery_mode_active` audit hook fires (stored as a sampled `recovery_mode` event), so it is explicit, auditable, and bounded (see §12.1 Phase R3). `WP_SUDO_RECOVERY_MODE` is 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)
553553
2. **Monitor and tune E2E CI acceleration without reducing release assurance**
554554
- Explicit CI groups now split the former long challenge shard across behavior-focused jobs
555555
- Keep the full Playwright suite available for release-grade confidence, but shorten normal feedback loops
@@ -1064,8 +1064,8 @@ SBOM, accessibility roadmap) are documented in the [CHANGELOG](../CHANGELOG.md).
10641064

10651065
### ✓ Shipped
10661066

1067-
**~~WP Activity Log (WSAL) Sensor Extension~~** — shipped v2.11.0 as `bridges/wp-sudo-wsal-sensor.php`. It now maps all 11 audit hooks to WSAL events (IDs 1900001–1900011). Inert when WSAL absent.
1068-
**~~Stream bridge~~** — implemented on `main` for v2.12.0 as `bridges/wp-sudo-stream-bridge.php`. Optional mu-plugin mapping for all 11 audit hooks.
1067+
**~~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.
1068+
**~~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).
10691069
**~~WP-CLI `wp sudo` commands~~** — implemented on `main` for v2.12.0 (`status`, `revoke --user`, `revoke --all`).
10701070
**~~Public `wp_sudo_check()` / `wp_sudo_require()` API~~** — implemented on `main` for v2.12.0 for third-party action gating integrations.
10711071
**~~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.
@@ -1083,8 +1083,34 @@ carries real risks (admin-file loading, multisite `wpmu_delete_user`, deletion /
10831083
post-reassignment hooks firing during an attack). Backlog: design an **optional,
10841084
opt-in sweep** (scheduled or `shutdown`-time) that reclaims never-completed roleless
10851085
users left by a blocked escalation — scoped to rows provably created-and-left-roleless
1086-
in a blocked request, reversible, and off by default. Only relevant once the guard
1087-
ships and is enabled.
1086+
in a blocked request, reversible, and off by default. The guard shipped in 4.1.0
1087+
(opt-in, default OFF), so this sweep is relevant whenever the guard is enabled.
1088+
1089+
**Escalation-guard blind spots (future mitigation — tracked, not yet scoped).**
1090+
The 4.1.0 admin-escalation guard hooks the `{prefix}capabilities` meta write and
1091+
`grant_super_admin`, so by construction it does **not** see three escalation
1092+
shapes. Tracked here so they are not forgotten:
1093+
1094+
1. **Runtime capability grants via `user_has_cap` / `map_meta_cap` filters.** A
1095+
plugin can confer administrator-equivalent capabilities per-request without ever
1096+
writing role/capabilities meta. A meta-write guard cannot observe this; a
1097+
capability-based check would catch custom admin-equivalent roles but carries a
1098+
high false-positive cost (it must distinguish legitimate per-request grants).
1099+
Needs a design before any attempt.
1100+
2. **Direct `$wpdb` writes to the usermeta table.** Raw SQL that sets the
1101+
capabilities row bypasses the metadata filters entirely. Anything with direct DB
1102+
access has broader reach already, so this is low-priority; a periodic
1103+
capabilities-integrity audit (compare stored caps against a known-good baseline)
1104+
is the only plausible mitigation and is out of scope for now.
1105+
3. **In-session residual window.** An escalation that fires *during* a legitimate
1106+
admin's own active sudo window (e.g. CSRF or stored XSS landing mid-session) is
1107+
allowed, because the actor holds a valid session. Mitigations would be
1108+
orthogonal (per-action re-prompt for the highest-risk grants, CSRF hardening) and
1109+
trade UX for coverage; tracked, not scheduled.
1110+
1111+
Bridge coverage is also incomplete: the WSAL/Stream bridges do not yet map the
1112+
4.1.0 `wp_sudo_escalation_blocked` event — adding it would let SIEM/audit tools
1113+
alert on escalation blocks directly. See the bridge-coverage backlog below.
10881114

10891115
### Open — High Priority Security Bridge Coverage
10901116

docs/admin-escalation-guard-analysis.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
# User Privilege-Escalation Guard — Implications Analysis
22

3-
> **Status: analysis / decision input.** Began as the deferred "Item 2"
4-
> (admin-surface escalation guard) from the gate-completeness work
5-
> (PRs #102/#104). Not implemented. **Scope has since evolved:** the analysis now
6-
> recommends an **effect-level** guard (hooked on the capabilities-meta write and
7-
> `grant_super_admin`) that covers *all* surfaces uniformly, rather than an
8-
> admin-`init`-only guard — because the exploit paths this defends against live on
9-
> REST/AJAX/unauthenticated surfaces, not the interactive admin (§1, §5). If
10-
> implemented, the guard ships **behind a filter defaulting OFF** (a recorded
11-
> decision), with a documented path to default-ON once escape hatches are proven
12-
> (§8). This document exists so the over-block surface is understood before any
13-
> code lands.
3+
> **Status: IMPLEMENTED — shipped in 4.1.0 (PR #111), opt-in / default OFF.**
4+
> Began as the deferred "Item 2" (admin-surface escalation guard) from the
5+
> gate-completeness work (PRs #102/#104). It is now the **effect-level** guard in
6+
> `includes/class-gate.php` (`arm_escalation_guard()`), hooked on the
7+
> capabilities-meta write (`add_user_metadata`/`update_user_metadata`) and
8+
> `grant_super_admin`, covering *all* surfaces uniformly — because the exploit
9+
> paths it defends against live on REST/AJAX/unauthenticated surfaces, not the
10+
> interactive admin (§1, §5). It ships **behind the `wp_sudo_guard_escalation`
11+
> filter, default `false` (OFF)**, with an allowlist filter
12+
> (`wp_sudo_allow_escalation`), the `WP_SUDO_ALLOW_ESCALATION` constant bypass, and
13+
> the high-severity `wp_sudo_escalation_blocked` action; the documented path to a
14+
> future default-ON is in §8. This document is the design rationale and over-block
15+
> analysis; the §7 scenarios were realized as tests in `tests/Unit/GateTest.php`.
16+
> Sections written in the future tense ("if implemented", "if approved") below
17+
> describe decisions that have since shipped as described.
1418
1519
## 1. The gap
1620

@@ -321,7 +325,7 @@ human to send to the challenge, so the only available response is a **hard block
321325
(halt the request before the write — §6/§9, not a short-circuit return), not a
322326
reauth prompt (§6, §8).
323327

324-
## 6. Implementation shape (if approved)
328+
## 6. Implementation shape (as shipped in 4.1.0)
325329

326330
- **Hooks (effect-level, surface-agnostic):** reuse the `add_user_metadata` /
327331
`update_user_metadata` filter on the `{prefix}capabilities` key; additionally
@@ -500,9 +504,9 @@ fail-open violates `CLAUDE.md`, fail-closed bricks role writes); and documenting
500504
the `user_has_cap` runtime-filter blind spot (§3) in the shipped FAQ, since it
501505
narrows the security claim to *meta-backed* role grants.
502506

503-
**Status:** design corrected; not approved for code. The three BLOCKERs are
504-
resolved at the design level here, but each must be proven by its §7 TDD scenario
505-
before and during implementation.
507+
**Status:** design corrected, approved, and **implemented in 4.1.0 (PR #111)**.
508+
The three BLOCKERs were resolved at the design level here and each was proven by
509+
its §7 TDD scenario in `tests/Unit/GateTest.php` during implementation.
506510

507511
## 10. Plain-language summary
508512

docs/current-metrics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Source: `.github/workflows/phpunit.yml`, `.github/workflows/e2e.yml`, `.github/w
6666

6767
## Verification Notes
6868

69-
- `composer test:unit` passed on 2026-06-23 (`834 tests`, `2399 assertions`).
69+
- `composer test:unit` passed on 2026-06-24 (`878 tests`, `2539 assertions`).
7070
- `composer lint` passed on 2026-06-20.
7171
- `composer analyse` passed on 2026-06-20 (PHPStan L6 `[OK] No errors`; Psalm `No errors found!`, 95.8% type coverage, baseline current).
7272
- `composer verify:metrics` passed on 2026-06-20 (after this update).

0 commit comments

Comments
 (0)