Skip to content

[6.x] Fix validation bypass via spoofed Precognition-Validate-Only header#14557

Merged
jasonvarga merged 5 commits into6.xfrom
precognition-header
Apr 28, 2026
Merged

[6.x] Fix validation bypass via spoofed Precognition-Validate-Only header#14557
jasonvarga merged 5 commits into6.xfrom
precognition-header

Conversation

@duncanmcclean
Copy link
Copy Markdown
Member

@duncanmcclean duncanmcclean commented Apr 27, 2026

This pull request fixes an issue where server-side validation could be bypassed by sending a Precognition-Validate-Only header on a non-precognitive request.

This was happening because filterPrecognitiveRules was checking for the Precognition-Validate-Only header instead of the Precognition header. A request with only Precognition-Validate-Only (and no Precognition: true) would pass through Laravel's HandlePrecognitiveRequests middleware as a real submission, but Statamic's validator would still filter rules based on that header — effectively dropping all validation.

This PR fixes it by checking the Precognition header instead, which is the header that actually indicates a precognitive request.

As a side effect of delegating to Laravel's Request::filterPrecognitiveRules(), precognition now correctly validates targeted subsets of nested form fields (grid, replicator, array, etc.). Laravel's JS client sends patterns like address.* or items.*.qty in the Precognition-Validate-Only header, and the helper compiles those into per-segment regex matches. The previous implementation used Collection::only(), which did literal-key lookups — so a request asking to validate address.* matched no rule keys (the actual keys look like address.line1, address.city) and stripped all rules, meaning precognition silently validated nothing for these field types. After this change those requests validate the intended subset.

Fixes #14556

Copy link
Copy Markdown
Member

@jasonvarga jasonvarga left a comment

Choose a reason for hiding this comment

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

via AI:

Thanks for catching the spoof — the underlying bug is real, but I think the fix as written introduces a worse regression and is worth reworking before merge.

The regression

Switching the header from Precognition-Validate-Only to Precognition breaks legitimate precognitive requests. Laravel's client sends Precognition: true, so:

explode(',', $request->header('Precognition'))   // ['true']
Collection::make($rules)->only(['true'])->all()  // []

…strips every rule. I confirmed by running the existing suite on this branch:

Tests\Tags\User\RegisterFormTest::it_handles_precognitive_requests
Expected response status code [422] but received 204.

So real precognition requests now skip validation entirely — strictly worse than the original spoof, since it affects every precognition flow rather than only attacker-crafted ones.

Suggested fix

Two pieces:

  1. Gate on $request->isPrecognitive() (set by Laravel's HandlePrecognitiveRequests middleware only after it confirms Precognition: true) — that closes the spoof.
  2. Delegate the actual filtering to Laravel's built-in helper $request->filterPrecognitiveRules($rules) (from the CanBePrecognitive trait).
public function filterPrecognitiveRules($rules)
{
    $request = request();

    if (! $request->isPrecognitive()) {
        return $rules;
    }

    return $request->filterPrecognitiveRules($rules);
}

Why delegate to Laravel's helper

For flat fields it's identical to the current Collection::only() approach. The difference is wildcard support for nested attributes — Laravel's helper compiles each pattern with *[^.]+, so Precognition-Validate-Only: address.* matches address.line1, items.*.qty matches items.0.qty, etc. Today Statamic's literal only() lookup finds no match for those and silently strips all rules, meaning precognition for forms with grid/replicator/array fields validates nothing. The delegation fixes that as a side-effect — worth calling out in the PR description as a stricter (correct) behavior shift for nested fields.

Tests

The added test only covers the spoof path. Could you also add a positive-path test — withPrecognition() + Precognition-Validate-Only: name should validate name and not email — so the legitimate flow has a safety net? The existing RegisterFormTest::it_handles_precognitive_requests is a good shape to copy.

duncanmcclean and others added 2 commits April 28, 2026 08:45
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jasonvarga jasonvarga merged commit cff13ff into 6.x Apr 28, 2026
17 checks passed
@jasonvarga jasonvarga deleted the precognition-header branch April 28, 2026 13:44
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.

Bot's able to fully bypass server side validation because of Precognition-Validate-Only header

2 participants