Skip to content

Run the full WordPress standard in phpcs - #760

Merged
obenland merged 1 commit into
trunkfrom
fix/security-sniff-coverage
Aug 18, 2026
Merged

Run the full WordPress standard in phpcs#760
obenland merged 1 commit into
trunkfrom
fix/security-sniff-coverage

Conversation

@obenland

Copy link
Copy Markdown
Member

What

phpcs.xml.dist composed WordPress-Core, WordPress-Docs and WordPress-Extra directly instead of referencing the WordPress standard. Only the meta standard carries the namespace="WordPressCS\WordPress" attribute that auto-includes every WordPressCS sniff — the sub-rulesets reference sniffs one by one, and WordPress-Extra only lists EscapeOutput, SafeRedirect and NonceVerification. So WordPress.Security.ValidatedSanitizedInput was never loaded.

Separately, <arg value="psn"/> reports errors only, which meant the nonce findings were warnings nobody saw.

Between the two, npm run lint:php reported zero security issues on trunk while 34 were present.

Ruleset changes

  • <rule ref="WordPress-Core"><rule ref="WordPress">, absorbing the one WordPress-Extra exclude.
  • WordPress-Docs block moved above the WordPress rule. WordPress includes WordPress-Docs itself, and whichever reference registers a sniff first is the one whose <exclude-pattern> applies. Declared after, the parser-file exemption is silently ignored and the block pattern parsers pick up 36 docblock errors. There's a comment on the block; it's worth keeping in mind if anyone reorders the file.
  • WordPress.Security promoted to <type>error</type>. Dropping the n from psn instead would surface 490 warnings, ~380 of them array alignment, which is a different piece of work.
  • text_domain moved off the comma-separated string syntax — deprecated in PHPCS 3.3.0, removed in 4.0. It was printing a DEPRECATED notice on every run.
  • Dropped Generic.Arrays.DisallowShortArraySyntax.Found and WordPress.PHP.DisallowShortTernary.Found. Both moved to PHPCSExtra in WPCS 3.0 and no longer matched anything; short ternaries are already covered by the working Universal.Operators.DisallowShortTernary rule at the bottom of the file. Confirmed no-ops — phpcs -e output is identical with and without them.

Checked with phpcs -e that the active sniff set is a superset of trunk: 182 → 186, nothing lost.

Code changes

One actual bug:

// themes/wporg-pattern-directory-2024/functions.php
'post_excerpt' => sanitize_text_field( $_POST['report-details'] ),

No wp_unslash(), and sanitize_text_field() doesn't unslash, so a report containing it's was stored as it\'s, compounding on re-save. Both report fields now go through guarded locals with isset() defaults.

The rest are unslash/sanitize on read-only request values in the theme patterns, the patterns list table and the pattern creator. locale() in pattern-translations was restructured so the value is unslashed once and compared against its own sanitized form, which keeps the existing reject-rather-than-coerce behaviour.

$_SERVER['REQUEST_URI'] uses esc_url_raw(). sanitize_text_field() would strip percent-encoded octets; checked against eight real REST URI shapes and esc_url_raw is byte-identical to raw on all of them.

Six phpcs:ignore/disable annotations remain — nonce false positives on read-only GET paths, plus one deliberate raw comparison in locale(). Each was checked by deleting it and re-running phpcs; all are load-bearing.

Tests

New coverage for locale() (9 tests) and display_post_states() (13). The test bootstrap now loads pattern-translations, which activates its filters for the whole suite — no effect on the existing tests, but worth knowing.

Both fixes were verified by reverting them and confirming a test fails. Worth noting because my first version of the display_post_states test passed against the broken code — asserting "no <script> in the output" is true either way. Rewrote it around sanitize_key() case-folding, which actually discriminates.

76 tests / 244 assertions, up from 54. phpcs exits 0.

Not covered

allow_reading_global_styles() early-returns unless REST_REQUEST is defined, and defining that constant would leak into every subsequent test, so it's untested.

Two pre-existing things noticed but left alone, both unchanged by this PR:

  • intval( $_POST['report-reason'] ) returns 1 for any non-empty array, so ?report-reason[]=99 assigns term ID 1 rather than rejecting.
  • strpos( $request_uri, '/wp/v2/global-styles' ) misses the ?rest_route=%2Fwp%2Fv2%2F... encoded form that plain-permalink sites use.

The ruleset composed WordPress-Core, WordPress-Docs and WordPress-Extra
directly rather than referencing the WordPress standard. Only the meta
standard carries the `namespace="WordPressCS\WordPress"` attribute that
pulls in every WordPressCS sniff, so WordPress.Security.ValidatedSanitizedInput
was never running. WordPress-Extra references EscapeOutput, SafeRedirect and
NonceVerification individually, which is all we had.

Switch to `<rule ref="WordPress">` and move the WordPress-Docs block above
it. WordPress includes WordPress-Docs itself, and the reference that
registers a sniff first is the one whose exclude-pattern applies, so the
parser-file exemption has to be declared first or the block pattern parsers
pick up docblock errors.

`<arg value="psn"/>` reports errors only, which hid the nonce warnings, so
promote WordPress.Security to error rather than dropping the `n` and
surfacing ~380 unrelated alignment warnings.

Also move `text_domain` off the comma-separated string syntax, deprecated
in PHPCS 3.3.0 and removed in 4.0, and drop two excludes naming sniffs that
moved to PHPCSExtra in WPCS 3.0 and no longer match anything.

That leaves 34 findings, fixed here:

* themes/.../functions.php passed $_POST['report-details'] to
  sanitize_text_field() without wp_unslash(), so slashes were persisting
  into flag excerpts. Both report fields now read through guarded locals.
* $_GET/$_REQUEST reads in the theme patterns, the patterns list table and
  the pattern creator are unslashed and sanitized.
* locale() unslashes once and compares the value against its own sanitized
  form, keeping the reject-rather-than-coerce behaviour.

Six annotations remain for nonce false positives on read-only GET paths and
one deliberate raw comparison. Each was checked by removing it and running
phpcs again.

Adds tests for locale() and display_post_states(); the bootstrap now loads
pattern-translations. Both fixes were checked by reverting them and
confirming the tests fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 03:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obenland
obenland merged commit e84ff78 into trunk Aug 18, 2026
3 checks passed
@obenland
obenland deleted the fix/security-sniff-coverage branch August 18, 2026 03:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants