@raishin/vanguard-frontier-agentic 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +7 -1
  3. package/.cursor-plugin/plugin.json +7 -1
  4. package/.github/plugin/marketplace.json +1 -1
  5. package/README.md +11 -11
  6. package/agents/cross-functional/revenue-critical-journey-integrity-agent/AGENT.md +112 -0
  7. package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/claude-code.agent.md +111 -0
  8. package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/codex.toml +37 -0
  9. package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/copilot.agent.md +120 -0
  10. package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/cursor.agent.md +112 -0
  11. package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/gemini.agent.md +112 -0
  12. package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/kiro-cli.agent.json +5 -0
  13. package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/kiro-ide.agent.md +111 -0
  14. package/agents/cross-functional/revenue-critical-journey-integrity-agent/metadata.json +42 -0
  15. package/agents/php/composer-supply-chain-agent/AGENT.md +114 -0
  16. package/agents/php/composer-supply-chain-agent/harnesses/claude-code.agent.md +113 -0
  17. package/agents/php/composer-supply-chain-agent/harnesses/codex.toml +119 -0
  18. package/agents/php/composer-supply-chain-agent/harnesses/copilot.agent.md +122 -0
  19. package/agents/php/composer-supply-chain-agent/harnesses/cursor.agent.md +114 -0
  20. package/agents/php/composer-supply-chain-agent/harnesses/gemini.agent.md +114 -0
  21. package/agents/php/composer-supply-chain-agent/harnesses/kiro-cli.agent.json +5 -0
  22. package/agents/php/composer-supply-chain-agent/harnesses/kiro-ide.agent.md +113 -0
  23. package/agents/php/composer-supply-chain-agent/metadata.json +31 -0
  24. package/agents/php/php-application-security-agent/AGENT.md +113 -0
  25. package/agents/php/php-application-security-agent/harnesses/claude-code.agent.md +112 -0
  26. package/agents/php/php-application-security-agent/harnesses/codex.toml +118 -0
  27. package/agents/php/php-application-security-agent/harnesses/copilot.agent.md +121 -0
  28. package/agents/php/php-application-security-agent/harnesses/cursor.agent.md +113 -0
  29. package/agents/php/php-application-security-agent/harnesses/gemini.agent.md +113 -0
  30. package/agents/php/php-application-security-agent/harnesses/kiro-cli.agent.json +5 -0
  31. package/agents/php/php-application-security-agent/harnesses/kiro-ide.agent.md +112 -0
  32. package/agents/php/php-application-security-agent/metadata.json +31 -0
  33. package/agents/php/php-maestro-agent/AGENT.md +81 -0
  34. package/agents/php/php-maestro-agent/harnesses/claude-code.agent.md +80 -0
  35. package/agents/php/php-maestro-agent/harnesses/codex.toml +86 -0
  36. package/agents/php/php-maestro-agent/harnesses/copilot.agent.md +89 -0
  37. package/agents/php/php-maestro-agent/harnesses/cursor.agent.md +81 -0
  38. package/agents/php/php-maestro-agent/harnesses/gemini.agent.md +81 -0
  39. package/agents/php/php-maestro-agent/harnesses/kiro-cli.agent.json +5 -0
  40. package/agents/php/php-maestro-agent/harnesses/kiro-ide.agent.md +80 -0
  41. package/agents/php/php-maestro-agent/metadata.json +31 -0
  42. package/agents/php/php-runtime-upgrade-readiness-agent/AGENT.md +117 -0
  43. package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/claude-code.agent.md +116 -0
  44. package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/codex.toml +122 -0
  45. package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/copilot.agent.md +125 -0
  46. package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/cursor.agent.md +117 -0
  47. package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/gemini.agent.md +117 -0
  48. package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/kiro-cli.agent.json +5 -0
  49. package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/kiro-ide.agent.md +116 -0
  50. package/agents/php/php-runtime-upgrade-readiness-agent/metadata.json +30 -0
  51. package/agents/php/wordpress-security-agent/AGENT.md +107 -0
  52. package/agents/php/wordpress-security-agent/harnesses/claude-code.agent.md +106 -0
  53. package/agents/php/wordpress-security-agent/harnesses/codex.toml +112 -0
  54. package/agents/php/wordpress-security-agent/harnesses/copilot.agent.md +115 -0
  55. package/agents/php/wordpress-security-agent/harnesses/cursor.agent.md +107 -0
  56. package/agents/php/wordpress-security-agent/harnesses/gemini.agent.md +107 -0
  57. package/agents/php/wordpress-security-agent/harnesses/kiro-cli.agent.json +5 -0
  58. package/agents/php/wordpress-security-agent/harnesses/kiro-ide.agent.md +106 -0
  59. package/agents/php/wordpress-security-agent/metadata.json +31 -0
  60. package/catalog/agents.json +175 -0
  61. package/catalog/asset-integrity.json +463 -43
  62. package/catalog/install-roles.json +26 -4
  63. package/catalog/model-assignments.json +198 -0
  64. package/catalog/skill-manifest.json +202 -0
  65. package/catalog/skills.json +163 -0
  66. package/package.json +1 -1
  67. package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
  68. package/powers/README.md +3 -2
  69. package/powers/vanguard-generic/POWER.md +1 -1
  70. package/powers/vanguard-php/POWER.md +40 -0
  71. package/schemas/agent.schema.json +2 -1
  72. package/schemas/skill.schema.json +2 -1
  73. package/scripts/generate-docs-data.mjs +1 -1
  74. package/skills/cross-functional/revenue-critical-journey-integrity-review/SKILL.md +108 -0
  75. package/skills/cross-functional/revenue-critical-journey-integrity-review/metadata.json +29 -0
  76. package/skills/cross-functional/revenue-critical-journey-integrity-review/references/idempotency-and-safe-retries.md +155 -0
  77. package/skills/cross-functional/revenue-critical-journey-integrity-review/references/official-sources.md +71 -0
  78. package/skills/cross-functional/revenue-critical-journey-integrity-review/references/pci-saq-scope-boundaries.md +118 -0
  79. package/skills/cross-functional/revenue-critical-journey-integrity-review/references/server-side-revalidation-trust-boundary.md +134 -0
  80. package/skills/cross-functional/revenue-critical-journey-integrity-review/references/webhook-delivery-dedup-ordering.md +146 -0
  81. package/skills/cross-functional/revenue-critical-journey-integrity-review/references/workflow-and-output.md +100 -0
  82. package/skills/php/composer-audit-supply-chain-review/SKILL.md +108 -0
  83. package/skills/php/composer-audit-supply-chain-review/metadata.json +20 -0
  84. package/skills/php/composer-audit-supply-chain-review/references/abandoned-and-advisory-governance.md +30 -0
  85. package/skills/php/composer-audit-supply-chain-review/references/composer-audit-policy.md +35 -0
  86. package/skills/php/composer-audit-supply-chain-review/references/lockfile-integrity.md +27 -0
  87. package/skills/php/php-maestro/SKILL.md +51 -0
  88. package/skills/php/php-maestro/metadata.json +20 -0
  89. package/skills/php/php-maestro/references/hard-gates-and-escalation.md +67 -0
  90. package/skills/php/php-maestro/references/routing-and-dispatch.md +91 -0
  91. package/skills/php/php-runtime-eol-opcache-fpm-review/SKILL.md +109 -0
  92. package/skills/php/php-runtime-eol-opcache-fpm-review/metadata.json +19 -0
  93. package/skills/php/php-runtime-eol-opcache-fpm-review/references/opcache-production-config.md +91 -0
  94. package/skills/php/php-runtime-eol-opcache-fpm-review/references/php-fpm-pool-tuning.md +87 -0
  95. package/skills/php/php-runtime-eol-opcache-fpm-review/references/php-version-lifecycle.md +102 -0
  96. package/skills/php/php-session-upload-deserialization-review/SKILL.md +111 -0
  97. package/skills/php/php-session-upload-deserialization-review/metadata.json +20 -0
  98. package/skills/php/php-session-upload-deserialization-review/references/file-upload-security.md +119 -0
  99. package/skills/php/php-session-upload-deserialization-review/references/session-security.md +126 -0
  100. package/skills/php/php-session-upload-deserialization-review/references/unserialize-object-injection.md +121 -0
  101. package/skills/php/wordpress-rest-block-security-review/SKILL.md +106 -0
  102. package/skills/php/wordpress-rest-block-security-review/metadata.json +20 -0
  103. package/skills/php/wordpress-rest-block-security-review/references/dynamic-block-output-escaping.md +42 -0
  104. package/skills/php/wordpress-rest-block-security-review/references/input-sanitize-output-escape.md +52 -0
  105. package/skills/php/wordpress-rest-block-security-review/references/rest-api-permission-callback.md +48 -0
  106. package/tests/fixtures/php-maestro-routing/expected/001-happy-application-security.json +6 -0
  107. package/tests/fixtures/php-maestro-routing/expected/002-happy-composer-supply-chain.json +6 -0
  108. package/tests/fixtures/php-maestro-routing/expected/003-happy-runtime-upgrade-readiness.json +6 -0
  109. package/tests/fixtures/php-maestro-routing/expected/004-happy-wordpress-security.json +6 -0
  110. package/tests/fixtures/php-maestro-routing/expected/005-happy-unserialize-session.json +6 -0
  111. package/tests/fixtures/php-maestro-routing/expected/006-happy-fpm-opcache.json +6 -0
  112. package/tests/fixtures/php-maestro-routing/expected/007-happy-composer-audit.json +6 -0
  113. package/tests/fixtures/php-maestro-routing/expected/008-happy-wp-permission-callback.json +6 -0
  114. package/tests/fixtures/php-maestro-routing/expected/adv-ambiguous.json +4 -0
  115. package/tests/fixtures/php-maestro-routing/expected/adv-instruction-injection.json +6 -0
  116. package/tests/fixtures/php-maestro-routing/expected/adv-liveguard-db-migration-prod.json +4 -0
  117. package/tests/fixtures/php-maestro-routing/expected/adv-liveguard-deploy-prod.json +4 -0
  118. package/tests/fixtures/php-maestro-routing/expected/adv-persona-replacement.json +6 -0
  119. package/tests/fixtures/php-maestro-routing/expected/adv-secrets-bait.json +7 -0
  120. package/tests/fixtures/php-maestro-routing/inputs/001-happy-application-security.json +7 -0
  121. package/tests/fixtures/php-maestro-routing/inputs/002-happy-composer-supply-chain.json +7 -0
  122. package/tests/fixtures/php-maestro-routing/inputs/003-happy-runtime-upgrade-readiness.json +7 -0
  123. package/tests/fixtures/php-maestro-routing/inputs/004-happy-wordpress-security.json +7 -0
  124. package/tests/fixtures/php-maestro-routing/inputs/005-happy-unserialize-session.json +7 -0
  125. package/tests/fixtures/php-maestro-routing/inputs/006-happy-fpm-opcache.json +7 -0
  126. package/tests/fixtures/php-maestro-routing/inputs/007-happy-composer-audit.json +7 -0
  127. package/tests/fixtures/php-maestro-routing/inputs/008-happy-wp-permission-callback.json +7 -0
  128. package/tests/fixtures/php-maestro-routing/inputs/adv-ambiguous.json +7 -0
  129. package/tests/fixtures/php-maestro-routing/inputs/adv-instruction-injection.json +7 -0
  130. package/tests/fixtures/php-maestro-routing/inputs/adv-liveguard-db-migration-prod.json +7 -0
  131. package/tests/fixtures/php-maestro-routing/inputs/adv-liveguard-deploy-prod.json +7 -0
  132. package/tests/fixtures/php-maestro-routing/inputs/adv-persona-replacement.json +7 -0
  133. package/tests/fixtures/php-maestro-routing/inputs/adv-secrets-bait.json +7 -0
  134. package/tests/fixtures/php-maestro-routing/taxonomy.json +69 -0
  135. package/tests/validate-catalog.py +1 -0
@@ -0,0 +1,126 @@
1
+ # Session security
2
+
3
+ ## Why this matters
4
+
5
+ A session id is a bearer credential: whoever presents it is treated as the
6
+ user it belongs to. Two distinct failures let an attacker end up holding a
7
+ victim's session id: fixation, where the attacker sets the session id before
8
+ the victim authenticates and it is never replaced, and hijacking, where the
9
+ attacker steals an already-authenticated session id after the fact (via
10
+ script injection, packet sniffing, or physical access). php.net's own
11
+ session-security guidance treats both as core, not edge-case, concerns, and
12
+ documents specific configuration and code-ordering requirements to close
13
+ each path.
14
+
15
+ ## NORMATIVE: regenerate the session id on privilege change
16
+
17
+ Per the php.net manual's session-management-basics guidance:
18
+
19
+ - "Session IDs **must** be regenerated when user privileges are elevated,
20
+ such as after authenticating. `session_regenerate_id()` must be called
21
+ prior to setting the authentication information to `$_SESSION`."
22
+ `session_regenerate_id()` saves the current session data automatically, so
23
+ the call itself does not lose session state — but it must come **before**
24
+ the authenticated flag is written, so that only the new session id carries
25
+ it.
26
+ - More generally: "Session ID regeneration reduces the risk of stolen
27
+ session IDs, thus `session_regenerate_id()` must be called periodically.
28
+ E.g. Regenerate the session ID every 15 minutes for security sensitive
29
+ content."
30
+ - `session_regenerate_id()` does **not** delete the outdated session's data
31
+ by default. The manual is explicit: "`session_regenerate_id()` does not
32
+ delete outdated sessions by default. Obsolete authenticated sessions may
33
+ be present for use. Developers must prevent outdated sessions to be
34
+ consumed by anyone." A regeneration call without also invalidating the
35
+ prior session's stored data leaves a still-valid, still-authenticated old
36
+ session id usable by anyone who captured it before regeneration.
37
+
38
+ ## NORMATIVE: `session.use_strict_mode` is mandatory and off by default
39
+
40
+ Per the manual: "enabling `session.use_strict_mode` is mandatory for secure
41
+ sessions. It is disabled by default. This prevents the session module [from
42
+ using] an uninitialized session ID. Put differently, the session module only
43
+ accepts valid session IDs generated by the session module. It rejects any
44
+ session ID supplied by users." With strict mode enabled, "an uninitialized
45
+ session ID is rejected and a new one is created. This prevents an attack that
46
+ forces users to use a known session ID" — the core session-fixation vector,
47
+ where an attacker "may paste links or send emails that contain the session
48
+ ID" and hope the victim's session simply adopts it.
49
+
50
+ Because this directive is **disabled by default**, a review must check for
51
+ an explicit `session.use_strict_mode=1` (or equivalent `ini_set()` call) —
52
+ absence of an error or warning is not evidence it is on.
53
+
54
+ ## NORMATIVE: cookie-hardening directives
55
+
56
+ Per the php.net manual's session INI-settings guidance, four directives
57
+ harden the session cookie itself:
58
+
59
+ - **`session.cookie_httponly`** — "Refuses access to the session cookie from
60
+ JavaScript. This setting prevents cookies snatched by a JavaScript
61
+ injection." The manual states: "Almost all applications must use the
62
+ httponly attribute for the session ID cookie."
63
+ - **`session.cookie_secure`** — "Allow access to the session ID cookie only
64
+ when the protocol is HTTPS. If a website is only accessible via HTTPS, it
65
+ should enable this setting." (The manual separately notes HSTS should be
66
+ considered for HTTPS-only sites.)
67
+ - **`session.cookie_samesite`** — "As of PHP 7.3 the `SameSite` attribute can
68
+ be set for the session ID cookie. This attribute is a way to mitigate CSRF
69
+ (Cross Site Request Forgery) attacks." `Lax` allows the cookie on a
70
+ cross-site GET navigation; `Strict` does not send it cross-site at all —
71
+ the manual frames the choice as that accessibility trade-off, not a
72
+ recommendation of one value for every application.
73
+ - Session hijacking generally (not fixation) is mitigated, per the manual,
74
+ by transport: "There are several ways to leak an existing session ID to
75
+ third parties. E.g. JavaScript injections, session IDs in URLs, packet
76
+ sniffing, physical access to the device, etc... The solution is to
77
+ implement SSL/TLS on the server and make it mandatory for users" —
78
+ `session.cookie_secure` is the configuration lever that enforces this for
79
+ the session cookie specifically.
80
+
81
+ ## Reviewer evidence criteria
82
+
83
+ For every authentication, privilege-elevation, or password-reset path:
84
+
85
+ - Confirm `session_regenerate_id(true)` is called, and confirm it is called
86
+ **before** the code writes the authenticated/elevated-privilege flag into
87
+ `$_SESSION`. A call present anywhere in the function is not sufficient —
88
+ check the ordering.
89
+ - Confirm the prior session's data is invalidated or is not relied upon as
90
+ still-authenticated after regeneration; a regeneration call with the old
91
+ session id left independently valid and authenticated is a finding.
92
+
93
+ For the effective session configuration in scope (`php.ini`, `.htaccess`,
94
+ `ini_set()` calls, or a framework's session configuration layer):
95
+
96
+ - Confirm `session.use_strict_mode` is explicitly enabled; do not treat its
97
+ absence from configuration as acceptable, since it is disabled by default.
98
+ - Confirm `session.cookie_httponly` is enabled.
99
+ - Confirm `session.cookie_secure` is enabled for any application served over
100
+ HTTPS (and flag its absence as a finding for an HTTPS-only application).
101
+ - Confirm `session.cookie_samesite` is set to `Lax` or `Strict` (requires
102
+ PHP 7.3+); flag an unset value, and flag PHP < 7.3 as unable to set this
103
+ directive at all (a version-upgrade finding, not a configuration finding).
104
+
105
+ Treat a missing `session_regenerate_id()` call (or one ordered after the
106
+ authenticated flag is set) and a missing/disabled `session.use_strict_mode`
107
+ as blocking findings. Treat missing `cookie_httponly`, `cookie_secure`, or
108
+ `cookie_samesite` as findings that escalate to blocking when combined with
109
+ any independently confirmed session-id-leak or fixation path.
110
+
111
+ ## Applicable versions
112
+
113
+ `session.cookie_samesite` requires PHP 7.3 or later — on an earlier version
114
+ this is a version-upgrade finding rather than a configuration omission. The
115
+ other directives and the `session_regenerate_id()` ordering guidance are
116
+ current php.net manual guidance as of this review; re-verify against the
117
+ live manual page before citing behavior for a PHP version outside current
118
+ support.
119
+
120
+ ## Sources
121
+
122
+ - [PHP Manual — Session Management Basics](https://www.php.net/manual/en/features.session.security.management.php) — supports the `session_regenerate_id()` ordering requirement around privilege elevation, the periodic-regeneration guidance, and the "does not delete outdated sessions by default" caution.
123
+ - [PHP Manual — Securing Session INI Settings](https://www.php.net/manual/en/session.security.ini.php) — supports `session.use_strict_mode` being mandatory-but-disabled-by-default, `session.cookie_httponly`, `session.cookie_secure`, and `session.cookie_samesite` (PHP 7.3+) directive behavior.
124
+ - [PHP Manual — Sessions and Security (overview)](https://www.php.net/manual/en/session.security.php) — top-level page framing session security as core to web application security and pointing to the two subsection pages above.
125
+
126
+ Last verified: 2026-07-16.
@@ -0,0 +1,121 @@
1
+ # Unserialize object injection
2
+
3
+ ## Why this matters
4
+
5
+ `unserialize()` does not just decode data — it can rebuild arbitrary objects
6
+ and then automatically hand them control. If the serialized string an
7
+ attacker controls names a class the application has autoloaded (directly or
8
+ transitively), reconstructing that object can invoke its magic methods with
9
+ attacker-influenced property values, and one of those methods reaching a
10
+ dangerous sink (a file write, a command, a second `unserialize()`, a query)
11
+ is enough to turn a data-parsing call into code execution. This is PHP
12
+ object injection, and php.net's own manual treats it as unconditional: there
13
+ is no safe way to call `unserialize()` on untrusted input, only safer
14
+ alternatives to use instead.
15
+
16
+ ## NORMATIVE: php.net's unconditional caution
17
+
18
+ Per the php.net manual page for `unserialize()`:
19
+
20
+ - "Do not pass untrusted user input to `unserialize()` regardless of the
21
+ `options` value of `allowed_classes`. Unserialization can result in code
22
+ being loaded and executed due to object instantiation and autoloading, and
23
+ a malicious user may be able to exploit this."
24
+ - The manual's stated alternative for untrusted data interchange is "a safe,
25
+ standard data interchange format such as JSON (via `json_decode()` and
26
+ `json_encode()`)."
27
+ - After successfully reconstructing an object, PHP "will automatically
28
+ attempt to call the `__unserialize()` or `__wakeup()` methods (if one
29
+ exists)" — this invocation happens as part of `unserialize()` itself, not
30
+ as a separate step a caller can skip.
31
+
32
+ This is the single controlling fact for this review: the caution is not
33
+ scoped to "when `allowed_classes` is absent" — it applies **regardless of**
34
+ that option's value. A review must not treat `allowed_classes` as the fix.
35
+
36
+ ## RECOMMENDATION: what `allowed_classes` actually does, and does not do
37
+
38
+ `allowed_classes` restricts which class names `unserialize()` is willing to
39
+ instantiate (e.g. `unserialize($string, ['allowed_classes' => false])`
40
+ instantiates no objects at all, converting them to `__PHP_Incomplete_Class`
41
+ instead). Use it as defense-in-depth when a design genuinely requires
42
+ `unserialize()` over data that cannot be fully untrusted-input-free, but do
43
+ not present it to engineers as a fix for untrusted-input reachability — the
44
+ manual's caution stands regardless of the option, and per user-contributed
45
+ notes on the same manual page, `allowed_classes` does not follow class
46
+ inheritance (allowing an interface does not allow its implementers, and
47
+ subclasses of an allowed class do not automatically pass).
48
+
49
+ ## The wider magic-method attack surface: `__destruct()` too
50
+
51
+ Object-injection ("PHP Object Injection" / gadget-chain) exploitation is not
52
+ limited to `__wakeup()`/`__unserialize()`. Any class instantiated by the
53
+ `unserialize()` call is a live PHP object with its full lifecycle ahead of
54
+ it, including destruction. Per the php.net manual for destructors: "The
55
+ destructor method will be called as soon as there are no other references to
56
+ a particular object, or in any order during the shutdown sequence." This
57
+ means a reconstructed object with attacker-controlled properties can trigger
58
+ consequential logic in its `__destruct()` even if it has no `__wakeup()` or
59
+ `__unserialize()` at all — including at request shutdown, when no other code
60
+ runs after it. Two destructor-execution details a reviewer should carry
61
+ into any gadget-chain analysis, per the same page:
62
+ `__destruct()` calls made during shutdown happen "in any order" (no
63
+ guaranteed sequence among unrelated objects), and destructors are called
64
+ even when the script stops via `exit()`. When reviewing a class reachable
65
+ from `unserialize()`, check `__wakeup()`, `__unserialize()`, and
66
+ `__destruct()` — all three are automatic, attacker-triggerable entry points
67
+ once the object exists.
68
+
69
+ ## RECOMMENDATION: prefer `json_decode()` for untrusted data
70
+
71
+ Match the manual's own stated remediation: for any data interchange that
72
+ crosses a trust boundary (request body, query string, cookie, cache key
73
+ sourced from user input, a queue message an external party can influence),
74
+ use `json_decode()`/`json_encode()` rather than `serialize()`/`unserialize()`.
75
+ If a legacy format genuinely requires PHP's native serialization for
76
+ internal, fully-trusted data only, confirm the value never round-trips
77
+ through anything a client can influence, and consider the manual's mention
78
+ of `hash_hmac()`-based integrity verification for data an application stores
79
+ externally and later reads back, so that tampering can be detected before
80
+ `unserialize()` (or `json_decode()`) is ever called on it.
81
+
82
+ ## Reviewer evidence criteria
83
+
84
+ For each `unserialize()` call site:
85
+
86
+ - Identify the exact source of the string passed in. Untrusted sources
87
+ include request parameters, headers, cookies, uploaded file contents,
88
+ values read back from a cache/session/queue backend that a client can
89
+ influence, and any database column a client-facing write path can reach.
90
+ - If the source is untrusted, this is a blocking finding regardless of
91
+ whether `allowed_classes` is set, per the manual's unconditional caution.
92
+ - If `allowed_classes` is present, do not close the finding on that basis;
93
+ note it as partial defense-in-depth only, and still require remediation
94
+ toward `json_decode()` or a fully-trusted data path.
95
+ - For any class the call could instantiate (open allowlist, `false`, or a
96
+ named list), check for `__wakeup()`, `__unserialize()`, and `__destruct()`
97
+ definitions in that class and its parents; if any exists, name it as a
98
+ live attack-surface entry point in the finding, not just a theoretical one.
99
+ - Confirm the recommended remediation is `json_decode()`/`json_encode()`
100
+ (or an equivalent non-object-instantiating format) unless the review
101
+ independently confirms native PHP serialization is required and the data
102
+ never crosses a trust boundary.
103
+
104
+ ## Applicable versions
105
+
106
+ The `unserialize()` caution, the automatic `__unserialize()`/`__wakeup()`
107
+ invocation, and the `allowed_classes` behavior described above are current
108
+ php.net manual guidance as of this review; the destructor shutdown-ordering
109
+ and `exit()` behavior is also current manual guidance, with the manual
110
+ separately noting that destructors are not run on shutdown caused by a fatal
111
+ error as of PHP 5.3.10. Re-verify against the live manual pages before citing
112
+ an exact figure for a PHP version outside current support, since documented
113
+ behavior can be refined between manual revisions.
114
+
115
+ ## Sources
116
+
117
+ - [PHP Manual — `unserialize()`](https://www.php.net/manual/en/function.unserialize.php) — supports the unconditional caution against passing untrusted input to `unserialize()` regardless of `allowed_classes`, the automatic `__unserialize()`/`__wakeup()` invocation on reconstructed objects, and the `json_decode()`/`json_encode()` remediation recommendation.
118
+ - [PHP Manual — Destructors](https://www.php.net/manual/en/language.oop5.decon.php) — supports the `__destruct()` invocation timing (on last reference removal or in any order during shutdown), invocation even after `exit()`, and the PHP 5.3.10 fatal-error exception to shutdown-time destructor execution.
119
+ - [OWASP Top 10:2021 — A08:2021 Software and Data Integrity Failures](https://owasp.org/Top10/2021/A08_2021-Software_and_Data_Integrity_Failures/index.html) — supports the OWASP category mapping for insecure-deserialization findings (CWE-502: Deserialization of Untrusted Data is listed among this category's notable weaknesses). OWASP has since published a Top 10:2025 edition (https://owasp.org/Top10/2025/) that renumbers this category to A08:2025 – Software or Data Integrity Failures and moves Injection to A05:2025; cite the specific edition and number in any finding rather than assuming 2021 numbering is still current.
120
+
121
+ Last verified: 2026-07-16.
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: wordpress-rest-block-security-review
3
+ description: Use this skill to review WordPress plugin, theme, REST API, and block-editor code for missing or overly permissive REST permission_callback registration, unescaped dynamic block render_callback output, missing input validation/sanitization or output escaping, and missing nonce/capability checks on state-changing requests. Use when reviewing register_rest_route() calls, dynamic block render_callback/render.php files, form/admin-ajax handlers, or any code path that takes untrusted input and produces WordPress-rendered output. Static review only; it does not execute plugin/theme code and never contacts a live, sandbox, or staging WordPress install.
4
+ allowed-tools: Read Grep Glob
5
+ metadata:
6
+ author: "github: Raishin"
7
+ version: "0.1.0"
8
+ updated: "2026-07-16"
9
+ category: security
10
+ lifecycle: experimental
11
+ ---
12
+
13
+ # WordPress REST & Block Security Review
14
+
15
+ ## Purpose
16
+
17
+ Review the WordPress-specific security seams that a generic PHP reviewer is likely to miss: REST routes registered without an explicit `permission_callback`, dynamic blocks whose `render_callback` emits data without escaping, input that is neither validated nor sanitized before use, and state-changing requests with no nonce or capability check. Plugins and themes are where the WordPress ecosystem's own advisory sources place the large majority of disclosed vulnerabilities, and each of these four gaps is a documented anti-pattern against WordPress's own core APIs, not a stylistic preference.
18
+
19
+ ## When to use
20
+
21
+ Use this skill when the user asks to:
22
+
23
+ - review a `register_rest_route()` call or a REST controller class for a missing or overly permissive `permission_callback`,
24
+ - review a dynamic block's `render_callback` or `render.php` for unescaped output,
25
+ - review input handling (REST params, `$_POST`/`$_GET`, shortcode or block attributes) for validation/sanitization gaps, or output for missing escaping,
26
+ - review a form submission, admin-ajax action, or REST write for missing nonce or capability checks.
27
+
28
+ ## When not to use
29
+
30
+ Do not use this skill for:
31
+
32
+ - generic PHP defects with no WordPress-specific dimension (raw SQL concatenation with no WordPress DB API involved, generic type-juggling, unrelated logic bugs) — use a general PHP security review for those.
33
+ - infrastructure or hosting-environment hardening (server configuration, TLS, hosting-provider WAF rules); this skill reviews source, not deployment environment.
34
+ - dependency/supply-chain review (vulnerable third-party libraries pulled in via Composer); hand that to a supply-chain reviewer.
35
+ - any live exercise of a WordPress install — executing plugin/theme code, submitting forms, calling REST endpoints, or triggering admin-ajax actions. This skill is static review only.
36
+
37
+ ## Preconditions
38
+
39
+ - The plugin or theme source in scope: REST route registrations, block registration and `render_callback`/`render.php` files, form handlers, admin-ajax actions, and shortcode callbacks.
40
+ - The WordPress version the code targets, where relevant to a version-gated API such as `permission_callback` enforcement.
41
+ - Whether any REST route or block output is genuinely intended to be public, so a correctly-scoped `__return_true` or unauthenticated render is not misflagged.
42
+
43
+ ## Lean operating rules
44
+
45
+ - Confirm `permission_callback` is present on every `register_rest_route()` call in scope, and that `__return_true` (or an equivalent always-true callback) is used only where the route is genuinely public.
46
+ - Trace every dynamic value in a block's `render_callback`/`render.php` from its source to its output point and confirm the matching escaping function is applied there, not merely present somewhere in the file.
47
+ - Require validation or sanitization of every untrusted input before use, favoring validation/rejection over sanitization alone where a specific check is possible.
48
+ - On non-REST state-changing handlers (form POSTs, `admin-post`/`admin-ajax` actions), require both an explicit nonce verification (`check_admin_referer`/`wp_verify_nonce`) and a `current_user_can()` capability check; neither alone is sufficient.
49
+ - For `register_rest_route()` endpoints, do not flag the absence of an in-handler nonce call: under cookie authentication the REST infrastructure verifies the `wp_rest` nonce automatically before the route runs, so the requirement there is a capability-checking `permission_callback`. A missing/invalid nonce silently demotes the request to anonymous, so it is a finding only when the `permission_callback` assumes an authenticated user.
50
+ - Never request, echo, store, or reproduce a secret, API key, credential, or credential-shaped string found in code; redact-and-flag it instead.
51
+ - Label every claim `repo evidence`, `documentation-based`, or `inference`.
52
+
53
+ ## Context7 documentation protocol
54
+
55
+ WordPress REST route requirements, nonce/capability semantics, and block-rendering behavior are documented on developer.wordpress.org and are the only acceptable ground truth for a version or API-behavior claim. Before asserting how `register_rest_route()`, `render_callback`, nonces, or escaping/sanitizing functions behave, cite the current developer.wordpress.org page and label the claim `documentation-based`. If a Context7-indexed source is available for the API in question, prefer it (`resolve-library-id` then `query-docs`) and label the result `context7-grounded`; otherwise use the official documentation directly. Never rely on memorized API behavior for a version-gated claim.
56
+
57
+ ## Workflow
58
+
59
+ 1. Enumerate the REST routes, dynamic blocks, and state-changing handlers in scope from the files provided.
60
+ 2. For each `register_rest_route()` call: confirm `permission_callback` is present; if it is `__return_true` or absent, confirm public-data justification or flag it. See [REST API permission_callback enforcement](references/rest-api-permission-callback.md).
61
+ 3. For each dynamic block `render_callback`/`render.php`: trace every dynamic value to its output point and confirm escaping is applied there. See [Dynamic block output escaping](references/dynamic-block-output-escaping.md).
62
+ 4. For each input source and output point in scope: confirm validation/sanitization on input and escaping on output, and confirm nonce plus capability checks on state-changing requests. See [Input validation, sanitization, and output escaping](references/input-sanitize-output-escape.md).
63
+ 5. Emit findings with evidence tiers, concrete exploit narratives, remediation, and verification steps; hand off any non-WordPress-specific finding to the owning reviewer.
64
+
65
+ ## Decision gates
66
+
67
+ - Block only on a finding with a demonstrated reachable path (unauthenticated REST access, unescaped render output, unchecked state change) — not on the mere absence of a keyword.
68
+ - Every WordPress-API version or behavior claim is `documentation-based` (or `context7-grounded` where available), never memory.
69
+ - Every finding names the specific route, block, or handler, not a general area of the codebase.
70
+ - Non-WordPress-specific findings are handed off, not adjudicated here.
71
+
72
+ ## Evidence classification
73
+
74
+ Label each finding `repo evidence` (seen directly in the code), `context7-grounded` (current documentation via Context7), `documentation-based` (official developer.wordpress.org documentation fetched directly), or `inference`. Documentation describes the intended pattern; it does not prove what a specific file actually does — always tie the claim back to the file and code path observed.
75
+
76
+ ## Security and privacy constraints
77
+
78
+ Static review only. Never execute plugin/theme code, submit forms, call REST endpoints, or trigger admin-ajax actions against any live, sandbox, or staging WordPress install. Never request, echo, store, or reproduce a secret, API key, database credential, or credential-shaped string found in code; treat any such string as a redact-and-flag finding.
79
+
80
+ ## Escalation conditions
81
+
82
+ Escalate to incident response any evidence the gap is already reachable in a live, publicly deployed site rather than merely present in source under review. Hand off non-WordPress-specific findings (generic PHP, infrastructure, supply-chain) to the reviewer who owns that area instead of adjudicating them here.
83
+
84
+ ## References
85
+
86
+ Load these only when needed:
87
+
88
+ - [REST API permission_callback enforcement](references/rest-api-permission-callback.md) — the required `permission_callback` argument, the `__return_true` public-endpoint pattern, and REST nonce/capability enforcement.
89
+ - [Dynamic block output escaping](references/dynamic-block-output-escaping.md) — how `render_callback`/`render.php` execute and what escaping is required at each output point.
90
+ - [Input validation, sanitization, and output escaping](references/input-sanitize-output-escape.md) — the validate-input/escape-output discipline, sanitizing and escaping function reference, and nonce/capability checks for state-changing requests.
91
+
92
+ ## Response minimum
93
+
94
+ Return, at minimum:
95
+
96
+ - the route(s), block(s), or handler(s) in scope and, per finding, the failure class and evidence tier;
97
+ - the concrete exploit narrative (how the gap is actually reachable, not just theoretically present);
98
+ - concrete remediation naming the specific WordPress function or pattern to add, and an exact verification step;
99
+ - handoffs for any non-WordPress-specific finding, and any incident-response escalation.
100
+
101
+ ## Anti-goals
102
+
103
+ - Do not expand into generic PHP review; own the WordPress-specific seam, hand off everything else.
104
+ - Do not execute plugin/theme code or contact any live, sandbox, or staging WordPress install.
105
+ - Do not echo, reproduce, or transmit any secret, credential, or credential-shaped string.
106
+ - Do not assert a WordPress API version or behavior from memory.
@@ -0,0 +1,20 @@
1
+ {
2
+ "id": "wordpress-rest-block-security-review",
3
+ "name": "WordPress REST & Block Security Review",
4
+ "type": "skill",
5
+ "provider": "php",
6
+ "harnesses": ["claude-code", "cursor", "codex", "gemini", "kiro", "other"],
7
+ "summary": "Skill for reviewing WordPress plugin and theme security: REST register_rest_route permission_callback enforcement (required since WordPress 5.5) and nonce/capability checks, dynamic-block render_callback output escaping, and the validate-on-input, escape-on-output discipline behind the plugin-dominated WordPress CVE surface.",
8
+ "source_type": "original",
9
+ "official_docs": [
10
+ "https://developer.wordpress.org/apis/security/",
11
+ "https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/",
12
+ "https://developer.wordpress.org/block-editor/getting-started/fundamentals/static-dynamic-rendering/",
13
+ "https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/"
14
+ ],
15
+ "security_notes": "Static-review-only skill: Read/Grep/Glob, no execution and no live WordPress access. Flags missing permission_callback, unescaped output, and missing nonce/capability checks from source only; treats any credential- or PII-shaped string as a redact-and-flag finding; grounds REST and block-editor API claims in current developer.wordpress.org documentation.",
16
+ "last_verified": "2026-07-16",
17
+ "path": "skills/php/wordpress-rest-block-security-review",
18
+ "author": "github: Raishin",
19
+ "version": "0.1.0"
20
+ }
@@ -0,0 +1,42 @@
1
+ # Dynamic block output escaping
2
+
3
+ ## Why this matters
4
+
5
+ A dynamic block's render function does not run once at build time — it runs on the server for every single front-end page view that includes the block. If that render function interpolates a dynamic value (a block attribute, post meta, a query result) into its returned markup without escaping, the resulting cross-site-scripting vector is not a one-off, one-visitor bug: it fires for every visitor, on every render, for as long as the block appears on the page. This is the single highest-severity, highest-blast-radius pattern this skill reviews for.
6
+
7
+ ## NORMATIVE: dynamic blocks render on the server at request time via render_callback or render.php
8
+
9
+ Per the WordPress Block Editor Handbook's "Static or Dynamic rendering" documentation (`documentation-based`): "Blocks with 'dynamic rendering' are designed to generate their content and structure in real-time when requested on the front end." The handbook identifies two supported ways to implement this: "Using the `render_callback` argument that can be passed to the `register_block_type()` function" and "Using a separate PHP file usually named `render.php`." It further states that on the front end, "the `render_callback` is used to dynamically render the markup for the block depending on the specific values on the server at the time the block is requested" — the render happens per-request, using live server-side values, not once at authoring time.
10
+
11
+ The same page's own example (the Site Title block's dynamic render) demonstrates the expected pattern directly: `esc_url()`, `esc_attr()`, and `esc_html()` are applied to dynamic values before they are placed into the returned markup. This is the documented reference pattern reviewers should compare a given `render_callback`/`render.php` against.
12
+
13
+ ## Reviewer evidence criteria
14
+
15
+ For every dynamic block `render_callback` or `render.php` in scope:
16
+
17
+ - Identify every dynamic value that reaches the returned markup: block attributes (`$attributes`), post/site data fetched via WordPress functions (`get_post_meta()`, `get_the_title()`, a custom query result), and any other value not authored as static markup in the block's own template.
18
+ - For each such value, confirm the escaping function matches its output context:
19
+ - text content inside an HTML element: `esc_html()`.
20
+ - a value printed into an HTML attribute: `esc_attr()`.
21
+ - a URL, including `src`/`href` attribute values: `esc_url()`.
22
+ - content that is itself expected to contain limited, allowed HTML (e.g. rendering a subset of post content): `wp_kses_post()` rather than `esc_html()`, which would strip the allowed markup entirely.
23
+ - Flag a dynamic value that reaches output with no escaping function applied at all as a direct site-wide XSS finding — the highest-severity finding class in this skill.
24
+ - Flag escaping applied earlier in the function (e.g. sanitizing on input) but not re-applied at the actual output point, since a value can be mutated or recombined between input and output; escaping must hold at the point of output, not merely somewhere upstream.
25
+ - Do not flag static markup authored directly in the block's template with no dynamic interpolation — escaping applies to dynamic values, not to literal markup the developer wrote.
26
+
27
+ ## RECOMMENDATION: how to phrase a dynamic-block escaping finding
28
+
29
+ - Name the block (registered block name) and the exact file/line in the `render_callback`/`render.php` where the unescaped value is emitted.
30
+ - Identify the dynamic value's source (attribute name, meta key, query) so the reviewer or maintainer can see exactly what an attacker-controlled or otherwise untrusted input would need to look like to exploit it.
31
+ - State the output context (HTML content / attribute / URL / limited-HTML content) and name the specific escaping function that context requires.
32
+ - Note that because dynamic block rendering executes on every front-end view, this is a standing, always-live vector once the block is published — not merely a reachable path found in review.
33
+
34
+ ## Applicable versions
35
+
36
+ - Block Editor Handbook static/dynamic rendering guidance: current Block Editor Handbook (`render_callback` and `render.php` as the two supported dynamic-rendering mechanisms). Re-verify against the current handbook before relying on details beyond what is quoted here, since block-editor APIs evolve across WordPress releases.
37
+
38
+ ## Sources
39
+
40
+ - [WordPress Block Editor Handbook — Static or Dynamic rendering of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/static-dynamic-rendering/) — supports the definition of dynamic rendering, the `render_callback`/`render.php` mechanisms, the per-request server-side execution model, and the Site Title block's `esc_url()`/`esc_attr()`/`esc_html()` reference example.
41
+
42
+ Last verified: 2026-07-16.
@@ -0,0 +1,52 @@
1
+ # Input validation, sanitization, and output escaping
2
+
3
+ ## Why this matters
4
+
5
+ WordPress's own security guidance names a single core model for every place a plugin or theme handles data: validate and sanitize on input, escape on output, and never trust any input regardless of its source. Most plugin and theme vulnerabilities are a violation of exactly this model at one specific point — a value that should have been checked or cleaned before use, or a value that should have been escaped where it was printed, was not. Reviewing against this model directly, rather than against a vague sense of "looks risky," is what makes a finding concrete and actionable.
6
+
7
+ ## NORMATIVE: validate/sanitize input, escape output, never trust input
8
+
9
+ Per the WordPress Developer Resources security overview (`documentation-based`): "Always make sure to validate and sanitize user input before using it, and to escape on output." The same page states its guiding principles directly: "Never trust user input," "Escape as late as possible," "Escape everything from untrusted sources (e.g., databases and users), third-parties (e.g., Twitter), etc.," and "Sanitation is okay, but validation/rejection is better." These principles apply regardless of the input's apparent trust level — including data read back from the WordPress database or from a third-party API, not only data taken directly from a request.
10
+
11
+ ## NORMATIVE: sanitizing input
12
+
13
+ Per the WordPress Developer Resources sanitizing-data documentation (`documentation-based`), sanitizing is "the process of securing/cleaning/filtering input data," and "all of it needs to be checked before it's used," including from sources that might otherwise be assumed trusted. Commonly applicable sanitizing functions the same page documents include `sanitize_text_field()`, `sanitize_textarea_field()`, `sanitize_email()`, `sanitize_file_name()`, `sanitize_url()`, and `wp_kses()`/`wp_kses_post()` for input that must retain a restricted set of HTML. The page's own example shows `sanitize_text_field()` performing UTF-8 validation, tag stripping, and removal of line breaks/tabs/extra whitespace — concrete, checkable behavior a reviewer can compare a given sanitization call's expected effect against. The same page restates the preference already noted above: "Validation is preferred over sanitization because validation is more specific" — when a value has a specific expected shape (an integer ID, an email address, an enumerated option), validate and reject rather than merely sanitize.
14
+
15
+ ## NORMATIVE: escaping output, as late as possible
16
+
17
+ Per the WordPress Developer Resources escaping-data documentation (`documentation-based`), escaping is "the process of securing output data by stripping out unwanted data, like malformed HTML or script tags." The page documents the core escaping functions by output context: `esc_html()` — "Use anytime an HTML element encloses a section of data being displayed. This will remove HTML"; `esc_attr()` — "Use on everything else that's printed into an HTML element's attribute"; `esc_url()` — "Use on all URLs, including those in the `src` and `href` attributes of an HTML element"; and `wp_kses_post()`, described as an "Alternative version of `wp_kses()` that automatically allows all HTML that is permitted in post content." The page states plainly: "It is best to do the output escaping as late as possible, ideally as data is being outputted" — escaping a variable once far from its output point, rather than inline at the point of output, is a documented anti-pattern, not merely a style preference, because it makes the correctness of the escaping harder to verify by inspection and easier to accidentally bypass with a later code change.
18
+
19
+ ## NORMATIVE: nonces and capability checks for state-changing requests
20
+
21
+ Per the WordPress Developer Resources nonces documentation (`documentation-based`), nonces "help protect URLs and forms from certain types of misuse, malicious or otherwise," and specifically "help protect against several types of attacks including CSRF." WordPress provides `wp_create_nonce()`, `wp_nonce_url()`, and `wp_nonce_field()` to generate nonces, and `check_admin_referer()` (checks the nonce and the referrer, for admin screens), `check_ajax_referer()` (checks the nonce but not the referrer, for AJAX requests), and `wp_verify_nonce()` (general-purpose verification) to check them. For REST requests specifically, the REST API Handbook's authentication documentation states nonces are transmitted via the `_wpnonce` parameter or the `X-WP-Nonce` header under the `wp_rest` action, and that a missing or invalid nonce demotes the request to an unauthenticated one rather than failing the request outright.
22
+
23
+ Per the WordPress Developer Resources user-roles-and-capabilities documentation (`documentation-based`): "As you build a plugin, make sure to run your code only when the current user has the necessary capabilities." The page's own contrasting example shows code with no capability check allowing "any visitor to the site to trash posts," against a corrected version gated by `current_user_can( 'edit_others_posts' )` — a nonce check alone confirms the request came from an expected context; it does not confirm the requester is authorized. Both checks are required together for a state-changing request: a nonce check without a capability check still permits an authenticated-but-unauthorized user to act, and a capability check without a nonce check is still forgeable via CSRF from an authenticated user's own browser.
24
+
25
+ ## Reviewer evidence criteria
26
+
27
+ - For every input source (REST params, `$_POST`/`$_GET`/`$_REQUEST`, shortcode attributes, block attributes, imported/uploaded data): confirm a validation or sanitization function is applied before the value is used in a query, a file path, a shell/command context, or output. An input used raw, or checked only with an `isset()`/type check with no actual sanitization/validation function, is a finding.
28
+ - Prefer validation (checking the value matches an expected, specific shape and rejecting it otherwise) over sanitization alone wherever the expected shape is specific enough to validate directly (an integer ID via `absint()`, an enumerated option checked against a whitelist).
29
+ - For every output point printing a value derived from user input, the database, or a third-party source: confirm the escaping function matches the output context (`esc_html()` for element content, `esc_attr()` for attributes, `esc_url()` for URLs, `wp_kses_post()` for content intentionally retaining limited HTML) and is applied at the point of output, not only earlier in the function.
30
+ - For every state-changing request (form submission, admin-ajax action, REST write): confirm both a nonce check (`wp_verify_nonce()`, `check_admin_referer()`, `check_ajax_referer()`, or REST nonce handling) and a `current_user_can()` capability check scoped to the specific action performed are present. Flag the absence of either one independently — do not treat one as substituting for the other.
31
+
32
+ ## RECOMMENDATION: how to phrase a validate/sanitize/escape finding
33
+
34
+ - Name the exact input source or output point and the file/line involved.
35
+ - State which half of the model is missing: input not validated/sanitized before use, or output not escaped at the point of output (or both).
36
+ - Name the specific WordPress function that belongs there (e.g. `sanitize_text_field()` on input, `esc_html()` at output, `current_user_can( '<capability>' )` plus a nonce check on a state-changing action) rather than a generic "add validation" instruction.
37
+ - Where a nonce or capability check is missing on a state-changing request, state which of the two is absent (or both), since each addresses a different attack (CSRF vs. unauthorized-but-authenticated access).
38
+
39
+ ## Applicable versions
40
+
41
+ - Guiding principles, sanitizing functions, and escaping functions: current WordPress Developer Resources security documentation. Re-verify against the current pages before relying on details beyond what is quoted here, since the function reference lists are maintained and can change.
42
+
43
+ ## Sources
44
+
45
+ - [WordPress Developer Resources — Security](https://developer.wordpress.org/apis/security/) — supports the core "validate and sanitize input, escape on output" model and the guiding principles ("Never trust user input," "Escape as late as possible," "Escape everything from untrusted sources," "Sanitation is okay, but validation/rejection is better").
46
+ - [WordPress Developer Resources — Sanitizing Data](https://developer.wordpress.org/apis/security/sanitizing/) — supports the sanitizing-function reference (`sanitize_text_field()`, `sanitize_email()`, `sanitize_file_name()`, `sanitize_url()`, `wp_kses()`/`wp_kses_post()`) and the validation-over-sanitization preference.
47
+ - [WordPress Developer Resources — Escaping Data](https://developer.wordpress.org/apis/security/escaping/) — supports the escaping-function reference (`esc_html()`, `esc_attr()`, `esc_url()`, `wp_kses_post()`) and the "escape as late as possible" principle.
48
+ - [WordPress Developer Resources — Nonces](https://developer.wordpress.org/apis/security/nonces/) — supports nonce creation (`wp_create_nonce()`, `wp_nonce_url()`, `wp_nonce_field()`) and verification (`wp_verify_nonce()`, `check_admin_referer()`, `check_ajax_referer()`) as CSRF protection.
49
+ - [WordPress Developer Resources — User Roles and Capabilities](https://developer.wordpress.org/apis/security/user-roles-and-capabilities/) — supports the `current_user_can()` capability-check pattern and the contrast between an unchecked action and one gated by a specific capability.
50
+ - [WordPress REST API Handbook — Authentication](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/) — supports REST-specific nonce transport (`_wpnonce`/`X-WP-Nonce` under the `wp_rest` action) and the requirement for a capability check beyond nonce verification.
51
+
52
+ Last verified: 2026-07-16.
@@ -0,0 +1,48 @@
1
+ # REST API permission_callback enforcement
2
+
3
+ ## Why this matters
4
+
5
+ A REST route is reachable by anyone who can send an HTTP request to it, regardless of whether it appears in a menu, an admin screen, or any UI at all. `register_rest_route()` treats the permissions callback as a first-class, separate argument from the main callback for exactly this reason: the main callback should handle the resource, and the permissions callback should decide who is allowed to reach it. Omitting that argument, or setting it to an always-true callback on a route that is not genuinely public, leaves the route open to anyone who finds it.
6
+
7
+ ## NORMATIVE: permission_callback is a required, checked argument
8
+
9
+ Per the WordPress REST API Handbook (`documentation-based`), `register_rest_route()` currently supports two callback arguments — `callback` and `permission_callback` — and describes their roles explicitly: "The main callback should handle the interaction with the resource. The permissions callback should handle what users have access to the endpoint." The handbook further states plainly that permissions callbacks "are extremely important for security with the WordPress REST API," and that any endpoint exposing data that should not be displayed publicly needs one registered.
10
+
11
+ Per the `register_rest_route()` function reference changelog (`documentation-based`), version **5.5.0** added a `_doing_it_wrong()` notice for the case where the required `permission_callback` argument is not provided. The notice text itself states the expectation directly: "The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback." That message names `__return_true` as the sanctioned pattern specifically for routes "intended to be public" — using it on a route that is not intended to be public is the documented anti-pattern this skill flags, not an inference.
12
+
13
+ ## Reviewer evidence criteria
14
+
15
+ Treat each of the following as a finding when reviewing a `register_rest_route()` call or a `WP_REST_Controller` subclass:
16
+
17
+ - No `permission_callback` key present in the route's args array at all.
18
+ - `permission_callback` set to `__return_true`, an inline closure that always returns `true`, or any other always-true callback, on a route that reads non-public data, accepts a state-changing HTTP method (`POST`/`PUT`/`PATCH`/`DELETE`), or otherwise is not genuinely intended for anonymous, public access.
19
+ - `permission_callback` present but checking a condition unrelated to authorization for the operation performed (e.g. checking that a parameter is set, rather than checking the requesting user's identity or capability).
20
+ - A permissions callback that itself trusts client-supplied data (e.g. a request parameter) to decide authorization, rather than checking the authenticated user or a server-side capability.
21
+
22
+ A `permission_callback` correctly scoped to a genuinely public, read-only, non-sensitive route (e.g. `__return_true` on a route that only returns already-public content) is not a finding — confirm the public-data justification before flagging.
23
+
24
+ ## NORMATIVE: REST authentication relies on nonces and capability checks together
25
+
26
+ Per the WordPress REST API Handbook's authentication documentation (`documentation-based`): "Cookie authentication is the standard authentication method included with WordPress" and "the REST API includes a technique called nonces to avoid CSRF issues." The nonce is transmitted as "the `_wpnonce` data parameter (either POST data or in the query for GET requests), or via the `X-WP-Nonce` header," created against "the action set to `wp_rest`." The handbook also states that "if no nonce is provided the API will set the current user to 0, turning the request into an unauthenticated request" — so a missing or invalid nonce does not error out; it silently demotes the request to an anonymous one, which is itself a review-relevant fact when a route's `permission_callback` assumes an authenticated user.
27
+
28
+ The same documentation states that beyond nonce verification (which the API performs automatically for you), "the current user must have the appropriate capability to perform the action being performed" — nonce verification proves the request came from an expected context, it does not by itself prove the user is authorized. A `permission_callback` must still perform its own capability check (e.g. `current_user_can()`) for any route that is not intended to be fully public.
29
+
30
+ ## RECOMMENDATION: how to phrase a permission_callback finding
31
+
32
+ - Name the route (path and HTTP method) and the exact file/line where `register_rest_route()` is called.
33
+ - State whether `permission_callback` is absent or set to an always-true callback, and whether the route was intended to be public — cite the code that suggests intent (e.g. handling of user-specific or write-capable data) rather than assuming.
34
+ - Give the concrete remediation: add a `permission_callback` that checks `current_user_can()` against the specific capability the operation requires, or confirm and document that the route is intentionally public.
35
+ - Note, where relevant, whether the endpoint also depends on nonce-based cookie authentication and whether a missing/invalid nonce would silently demote the request to anonymous rather than fail closed.
36
+
37
+ ## Applicable versions
38
+
39
+ - `permission_callback` enforcement via `_doing_it_wrong()` notice: WordPress 5.5.0 and later.
40
+ - REST cookie authentication and the `wp_rest` nonce action: current WordPress REST API Handbook, re-verify against the handbook before relying on details beyond what is quoted here.
41
+
42
+ ## Sources
43
+
44
+ - [WordPress REST API Handbook — Routes and Endpoints](https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/) — supports the `callback`/`permission_callback` argument split, the description of what each callback does, and the handbook's own framing of permissions callbacks as extremely important for security.
45
+ - [WordPress REST API Handbook — Authentication](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/) — supports cookie authentication as the standard method, the `wp_rest` nonce action, the `X-WP-Nonce`/`_wpnonce` transport mechanism, the silent demotion to user 0 on a missing/invalid nonce, and the requirement for a capability check beyond nonce verification.
46
+ - [WordPress function reference — register_rest_route()](https://developer.wordpress.org/reference/functions/register_rest_route/) — supports the 5.5.0 changelog entry adding a `_doing_it_wrong()` notice for a missing required `permission_callback` argument, and the notice's own text naming `__return_true` as the pattern for routes intended to be public.
47
+
48
+ Last verified: 2026-07-16.
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "php-application-security-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "composer-supply-chain-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "php-runtime-upgrade-readiness-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "wordpress-security-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "php-application-security-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "php-runtime-upgrade-readiness-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "composer-supply-chain-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "wordpress-security-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "route": [],
3
+ "mode": "unclassified"
4
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "route": [
3
+ "php-application-security-agent"
4
+ ],
5
+ "mode": "single"
6
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "route": [],
3
+ "mode": "live-guard-gate"
4
+ }