[6.x] Fix validation bypass via spoofed Precognition-Validate-Only header#14557
[6.x] Fix validation bypass via spoofed Precognition-Validate-Only header#14557jasonvarga merged 5 commits into6.xfrom
Conversation
jasonvarga
left a comment
There was a problem hiding this comment.
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:
- Gate on
$request->isPrecognitive()(set by Laravel'sHandlePrecognitiveRequestsmiddleware only after it confirmsPrecognition: true) — that closes the spoof. - Delegate the actual filtering to Laravel's built-in helper
$request->filterPrecognitiveRules($rules)(from theCanBePrecognitivetrait).
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.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This pull request fixes an issue where server-side validation could be bypassed by sending a
Precognition-Validate-Onlyheader on a non-precognitive request.This was happening because
filterPrecognitiveRuleswas checking for thePrecognition-Validate-Onlyheader instead of thePrecognitionheader. A request with onlyPrecognition-Validate-Only(and noPrecognition: true) would pass through Laravel'sHandlePrecognitiveRequestsmiddleware 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
Precognitionheader 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 likeaddress.*oritems.*.qtyin thePrecognition-Validate-Onlyheader, and the helper compiles those into per-segment regex matches. The previous implementation usedCollection::only(), which did literal-key lookups — so a request asking to validateaddress.*matched no rule keys (the actual keys look likeaddress.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