Skip to content

Fix: harden smart tag resolution against post-submission injection - #1669

Open
lihsaa591 wants to merge 3 commits into
developfrom
fix/smart-tag-injection-post-meta-disclosure
Open

lihsaa591 wants to merge 3 commits into
developfrom
fix/smart-tag-injection-post-meta-disclosure

Conversation

@lihsaa591

Copy link
Copy Markdown
Contributor

Background

Wordfence reported (Everest Forms <= 3.6.1): unauthenticated visitors could read arbitrary non-protected post/user meta by injecting a smart tag like {post_meta key=...} into a form field. #1667 attempted a fix but the patch was rejected — Wordfence reproduced a bypass against it.

What was missed in #1667

#1667 made two changes:

  1. Restricted post-submission smart tag re-resolution to type === 'hidden' fields.
  2. Added an is_protected_meta() check before resolving post_meta/posts_meta_current_page_id/user_meta tags.

The bypass: a Hidden field's submitted value is still just a normal POST parameter — nothing stops a request from setting it directly, regardless of field type. Restricting by type doesn't stop the value from being attacker-controlled, so the same payload placed in a form's hidden field (e.g. a UTM-tracking field) still got resolved and echoed back via the entry preview.

Fix

  • do_task() / entry_save() now resolve smart tags only from the field's own admin-configured default_value (the trusted form definition), never from the submitted value. A crafted request body can no longer influence what gets resolved.
  • Found and fixed a second, related bypass while investigating: {field_id="X"} merge tags (used to embed a submitted field's value into notification/confirmation templates) were substituted in before the generic tag scan ran. That meant typing {post_meta key=...} into an ordinary visible field — not just hidden ones — could get re-resolved if any template referenced that field. Generic tags now resolve first against the original trusted template, and {field_id} substitution happens last with no further re-scanning.
  • Kept the is_protected_meta() check from Fix: tighten smart tag resolution scope and meta-key access #1667 as an additional layer.

Test plan

  • Verified live against the real plugin code/DB: original PoC payload no longer resolves, the {field_id} bypass no longer resolves, protected (_-prefixed) meta stays blocked.
  • No regressions: admin-authored {post_meta} tags still resolve, and a Hidden field's default_value containing {field_id="X"} (the legitimate use case) still resolves correctly.
  • Added tests/phpunit/includes/class-evf-smart-tags-security-test.php covering all of the above.
  • PHPCS clean on the changed files.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

The prior fix (#1667) restricted post-submission smart tag re-resolution
to Hidden fields, but a Hidden field's POST value is just as
attacker-controllable as any other field, so it didn't close the hole.

Resolve smart tags strictly from the field's own admin-configured
default_value (the trusted form definition) instead of the submitted
value, in both do_task() and entry_save().

Also fixes a related bypass: {field_id="X"} merge tags (used to embed a
submitted field's value in notification/confirmation templates) were
substituted in before the generic smart tag scan ran, so typing a tag
like {post_meta key=...} into any ordinary visible field referenced by
such a template would get it re-resolved. Generic tags now resolve
first, against the original trusted template, with {field_id}
substitution done last and never re-scanned.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tg-autopilot
tg-autopilot requested a lite review from Copilot September 29, 2026 09:06
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

QA suite — BROKEN, not passed

The runner executed 0 tests. That is a broken harness, not a pass —
do not read this as the change being safe.

Likely causes: the grep matched nothing, spec_dir is wrong, or the
install left no runner. The step log says which.

Automated check — no AI involved. It runs the tests in this branch.

@tg-autopilot

Copy link
Copy Markdown
Contributor

Build for bccac214 is ready 🛎️

⬇️ Download everest-forms.zip (11M)

Installs directly via Plugins → Add New → Upload Plugin.
Link expires in 30 days · updated Sep 29, 2026 2:55 PM +0545

A later push only rebuilds this if its commit message includes #build-zip.

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 review overview

🟡 Changes recommended

Regression tests do not execute the production task/save paths, leaving the security fix insufficiently covered.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Hardens smart-tag resolution against attacker-controlled submitted values and adds security regression coverage.

Changes:

  • Resolves tags from trusted configured defaults.
  • Prevents rescanning {field_id} substitutions.
  • Retains protected-meta checks.
  • Adds injection regression tests.
File Summary
tests/​phpunit/​includes/​class-evf-smart-tags-security-test.php Adds smart-tag security regression tests.
includes/​class-evf-smart-tags.php Reorders resolution to prevent rescanning submitted values.
includes/​class-evf-form-task.php Uses trusted defaults during submission and entry saving.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/phpunit/includes/class-evf-smart-tags-security-test.php Outdated
Guards the {field_id} bypass (the second, previously-unreported issue
fixed alongside #1667's gap): a visitor's own field value embedded into
an admin-authored redirect query string via {field_id="X"} must stay
literal text, never re-resolved as a smart tag.

Proven against both versions: 3/3 pass on the fix, fails on the
pre-fix code with the real admin email leaking through the redirect
query string (`Expected: "{admin_email}" Received: "qa@example.test"`).

Does not cover the Hidden-field default_value path (#1667's original
gap) — that's guarded at the unit level in
tests/phpunit/includes/class-evf-smart-tags-security-test.php, since it
needs a Hidden field with a specific default_value and this suite has
no builder-driven fixture for adding a custom field yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lihsaa591

Copy link
Copy Markdown
Contributor Author

Added the requested regression spec: `tests/e2e/specs/submission/field-value-smart-tag-not-reresolved.spec.ts`.

It guards the `{field_id}` bypass (the second, previously-unreported issue fixed in this PR alongside #1667's gap): a visitor-typed smart tag embedded into an admin-authored redirect query string via `{field_id="X"}` must stay literal text.

Proven against both versions locally (fresh Playground boot):

  • Fixed code: 3/3 pass
  • Pre-fix code: fails with the real admin email leaking through the redirect — `Expected: "{admin_email}" Received: "qa@example.test"`

Also added a PHPUnit test (`tests/phpunit/includes/class-evf-smart-tags-security-test.php`) covering both fixed paths at the unit level, including the Hidden-field `default_value` path from #1667's original gap, which the e2e suite has no builder fixture to drive yet.

@lihsaa591

Copy link
Copy Markdown
Contributor Author

@claudegrill suite

@lihsaa591

Copy link
Copy Markdown
Contributor Author

The `@claudegrill suite` run just now (run 36556142064) failed with `exit 2` — a harness failure, not a test failure: `ran_nothing: true`, `reason: "the runner executed 0 tests"`.

Root cause is a shell-quoting bug in the shared `ThemeGrill/claudegrill` reusable workflow, not in this PR: since the latest commit here only touched a new spec file, the scope computation set

```
SCOPE_ARGS="--spec 'tests/e2e/specs/submission/field-value-smart-tag-not-reresolved.spec.ts'"
```

with the single quotes baked into the string, then expanded it unquoted (`$SCOPE_ARGS`) in the `run-suite.mjs` call. Bash word-splits it and the literal quote characters end up glued onto the path, so Playwright matches no file and runs nothing. This would hit any PR whose latest commit is spec-only — it's a bug in that shared workflow's scope script, not something fixable from this repo.

Locally (fresh Playground boot, documented above): fixed code passes 3/3, and the spec fails against the pre-fix code with the real admin email leaking through the redirect (`Expected: "{admin_email}" Received: "qa@example.test"`). The fix and its regression coverage are sound; this is a CI tooling gap in `ThemeGrill/claudegrill` that needs a fix on that side.

@lihsaa591

Copy link
Copy Markdown
Contributor Author

Root-caused and fixed the shared workflow bug: themegrill/claudegrill#8. Once merged there, @claudegrill suite should run this PR's new spec correctly.

…ests

Copilot flagged (correctly) that the two hidden-field regression tests
called a test-local copy of the resolution loop instead of the actual
EVF_Form_Task code, so reverting either production fix would still
leave them green.

Rewritten to drive the real entry point: a genuine `everest_form` post
is created, submitted through evf()->task->do_task() with a real nonce,
and assertions read both the in-memory $task->form_fields and the
persisted evf_entrymeta row back from the database -- exercising
do_task() and entry_save() exactly as a live request does.

Doing this surfaced two small pre-existing bugs in evf_get_browser()
(unrelated to the smart-tag fix, just never exercised by any prior
test that reaches entry_save()): an uninitialized $ub used before
assignment when the user agent matches no known browser, and an
undefined array offset when zero regex matches are found. Both are
now guarded.

Verified locally: all 5 tests pass (temporarily patched the repo's
independently-broken PHPUnit bootstrap -- missing WP_Mock/phpunit-
polyfills dev dependencies, unrelated to this change -- to actually
run them; that patch is not part of this commit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@deepench deepench left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 👍

@lihsaa591

Copy link
Copy Markdown
Contributor Author

Audited the e2e spec (tests/e2e/specs/submission/field-value-smart-tag-not-reresolved.spec.ts) against the same criterion Copilot raised for the PHPUnit test — does it exercise the real production path, or a test-local copy of the logic?

It's already a genuine black-box test with no duplicated logic:

  • Configures the redirect/query-string settings through the real wp-admin builder UI (saves via the actual everest_forms_save_form AJAX endpoint).
  • Submits as a real anonymous visitor via a real HTTP POST to the actual front-end handler (EVF_Form_Task::do_task()).
  • Follows the real, server-computed redirect URL in the browser and asserts directly on it — nothing re-implemented that could diverge from production.

Already proven (documented above): fails against the pre-fix code with the real admin email leaking, passes 3/3 on the fix. No change needed here.

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.

4 participants