Skip to content

Answer the WordPress.org review: consent, permissions, and sanitising early - #47

Merged
AllTerrainDeveloper merged 3 commits into
mainfrom
fix/wporg-review-round-2
Aug 28, 2026
Merged

AllTerrainDeveloper merged 3 commits into
mainfrom
fix/wporg-review-round-2

Conversation

@AllTerrainDeveloper

Copy link
Copy Markdown
Owner

The second WP.org review round found four issues. Each is fixed at its root, with a regression test.

🔴 Phoning home — Akismet on by default (Guidelines 7 & 9)

New forms enabled Akismet by default, so a site that already had Akismet configured transmitted submission contents, IPs and user agents without anyone opting in for that form.

  • alltfo_default_schema() now ships spam.akismet => false. Switching it on is a per-form decision by the site owner — which is what readme.txt always claimed under External services; the default now matches the claim.
  • The builder's checkbox says exactly what switching it on does: what is sent, and where.
  • Docs (hooks-reference.md), README.md, readme.txt and the spam.php header updated to match.

🔴 Permission callbacks on abilities and the /track route

  • get-form / list-entries / form-report — the permission callback now receives the ability input (the Abilities API passes it whenever an input schema is declared) and asks alltfo_can_read_entries( $form_id ) about that form, so the per-form filter a site uses to confine a department actually confines these abilities. list-forms filters what it lists through the same gate.
  • submit-form — no longer __return_true. It requires an authenticated user, because the ability mints a valid time-trap signature and that liberty is only honest on a channel that identifies its caller. Anonymous visitors still have the rendered form and the public REST /submit route, where every anti-bot check runs whole. Any logged-in user may submit — submitting is what visitors do.
  • /track — the handler now requires publish status, so the public route can neither probe nor inflate drafts, trash or archives. It still answers 200 regardless, never confirming what exists.

docs/abilities.md permission table updated to match.

🔴 Sanitize early

The raw $_POST reached the alltfo_before_submission action before field-type sanitisation ran. A new alltfo_sanitize_request() deep-sanitises the whole request — keys included — at the top of alltfo_process_submission(), before any code reads a byte of it, covering the no-JS POST path, the REST route and the abilities in one place. Each answer is still sanitised again through its own field type; this pass is about never holding dirty data, that one about the shape each field expects. The sanitiser is registered with PHPCS as a customSanitizingFunctions entry, so the previously-ignored sniff now passes on its own.

Tests

Six new tests: Akismet default is off (a regression here is a privacy bug), logged-out callers are refused everywhere including submit-form, a subscriber can submit, the per-form filter confines get-form/form-report/list-forms, /track bumps only published forms, and the request arrives sanitised at the first hook.

738 PHPUnit ✓ · 372 vitest ✓ · PHPCS 0 errors (no new warnings) · Plugin Check: no errors

🤖 Generated with Claude Code

https://claude.ai/code/session_01DVAaHFURDGeBzeuWHZ2dcP

AllTerrainDeveloper and others added 3 commits August 28, 2026 23:49
… early

Four findings from the second review round, each fixed at its root:

- Akismet is now off by default on every form. Sending a submission — the
  answers, the sender's IP and user agent — to Akismet's servers is the site
  owner's per-form decision, never a default the plugin makes for them. The
  builder's checkbox now says exactly what switching it on sends.

- The abilities ask their permission question about the form in hand. The
  get-form, list-entries and form-report gates pass the input's form_id to
  the alltfo_can_read_entries filter, list-forms filters what it lists per
  form, and submit-form requires an authenticated user — the minted time-trap
  signature was only ever honest on a channel that identifies its caller.

- The public /track route bumps only published forms, so it can no longer
  probe or inflate drafts, trash or archives.

- The submission request is deep-sanitised at the top of the pipeline,
  before the alltfo_before_submission action or anything else reads a byte
  of it; each answer is still sanitised again through its own field type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVAaHFURDGeBzeuWHZ2dcP
The no-JS fallback kses'd the confirmation at output while the AJAX path
handed the resolved string straight to the bundle's innerHTML, trusting the
field-type sanitisers alone to have stripped every tag from the values the
merge tags splice in. They do — but two paths to one string deserve one
armour, so the message is now kses'd at resolution, where both paths read it.

With a regression test that submits a script-and-onerror payload through a
merge-tagged confirmation and asserts the payload is gone entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVAaHFURDGeBzeuWHZ2dcP
wp_http_validate_url() ran once, on the configured URL — and then
wp_remote_post() followed any redirect the endpoint chose to answer with,
internal addresses included. wp_safe_remote_post() re-validates each hop,
which closes the 302-to-somewhere-private gap without changing what a
well-behaved webhook sees.

With the plugin's first webhook transport test, pinning reject_unsafe_urls
and the HMAC signature header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVAaHFURDGeBzeuWHZ2dcP
@AllTerrainDeveloper
AllTerrainDeveloper merged commit baa07d1 into main Aug 28, 2026
3 checks passed
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.

1 participant