From c8d993a731c90212b9389edb230a6dce99894feb Mon Sep 17 00:00:00 2001 From: NurullahDemirel Date: Sun, 29 Mar 2026 13:25:08 +0300 Subject: [PATCH 1/6] strict mode for validation --- config/validation.php | 5 + .../Foundation/Http/FormRequest.php | 122 ++++++++++++ .../Foundation/FoundationFormRequestTest.php | 174 +++++++++++++++++- 3 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 config/validation.php diff --git a/config/validation.php b/config/validation.php new file mode 100644 index 000000000000..ae8f45374065 --- /dev/null +++ b/config/validation.php @@ -0,0 +1,5 @@ + false, +]; diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 3f12c10a1aff..826d644de059 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -14,6 +14,7 @@ use Illuminate\Foundation\Http\Attributes\StopOnFirstFailure; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; +use Illuminate\Support\Arr; use Illuminate\Validation\ValidatesWhenResolvedTrait; use ReflectionClass; @@ -70,6 +71,22 @@ class FormRequest extends Request implements ValidatesWhenResolved */ protected $stopOnFirstFailure = false; + /** + * Per-class override for rejecting fields not defined in rules(). + * + * Null falls through to the global flag or the config value. + * + * @var bool|null + */ + protected ?bool $failOnUnknownFields = null; + + /** + * Global flag set via FormRequest::failOnUnknownFields(). + * + * @var bool + */ + protected static bool $globalFailOnUnknownFields = false; + /** * The validator instance. * @@ -77,6 +94,20 @@ class FormRequest extends Request implements ValidatesWhenResolved */ protected $validator; + /** + * Enable or disable unknown-field rejection globally for all form requests. + * + * Usage in AppServiceProvider::boot(): + * FormRequest::failOnUnknownFields(! app()->isProduction()); + * + * @param bool $value + * @return void + */ + public static function failOnUnknownFields(bool $value = true): void + { + static::$globalFailOnUnknownFields = $value; + } + /** * Get the validator instance for the request. * @@ -109,6 +140,12 @@ protected function getValidatorInstance() )); } + if ($this->shouldFailOnUnknownFields()) { + $validator->after(function (Validator $validator) { + $this->validateNoExtraFields($validator); + }); + } + $this->setValidator($validator); return $this->validator; @@ -146,6 +183,91 @@ protected function configureFromAttributes() } } + /** + * Determine if fields not present in rules() should fail validation. + * + * Resolution order: + * 1. $failOnUnknownFields → per-class override + * 2. $globalFailOnUnknownFields → FormRequest::failOnUnknownFields() ile set edilen global değer + * 3. Config "validation.fail_on_unknown_fields" + * + * @return bool + */ + protected function shouldFailOnUnknownFields(): bool + { + if ($this->failOnUnknownFields !== null) { + return $this->failOnUnknownFields; + } + + if (static::$globalFailOnUnknownFields) { + return true; + } + + if ($this->container->bound('config')) { + return (bool) $this->container->make('config')->get('validation.fail_on_unknown_fields', false); + } + + return false; + } + + /** + * @param \Illuminate\Contracts\Validation\Validator $validator + * @return void + */ + protected function validateNoExtraFields(Validator $validator): void + { + $allowedKeys = array_keys($this->container->call([$this, 'rules'])); + + $inputKeys = array_keys(Arr::dot($this->all())); + + foreach ($inputKeys as $inputKey) { + if (! $this->isAllowedKey($inputKey, $allowedKeys)) { + $validator->errors()->add( + $inputKey, + $this->getFailOnUnknownFieldsMessage($inputKey) + ); + } + } + } + + /** + * @param string $inputKey The dot-notation key from the request input. + * @param array $allowedKeys The keys defined in rules(). + * @return bool + */ + protected function isAllowedKey(string $inputKey, array $allowedKeys): bool + { + foreach ($allowedKeys as $ruleKey) { + if ($ruleKey === $inputKey) { + return true; + } + + if (str_contains($ruleKey, '*')) { + $pattern = '/^'.str_replace( + ['\*', '\.'], + ['[^.]+', '\.'], + preg_quote($ruleKey, '/') + ).'$/'; + + if (preg_match($pattern, $inputKey)) { + return true; + } + } + } + + return false; + } + + /** + * @param string $field The name of the unexpected field. + * @return string + */ + protected function getFailOnUnknownFieldsMessage(string $field): string + { + return trans('validation.prohibited', ['attribute' => str_replace('_', ' ', $field)]) + ?: "The {$field} field is not allowed."; + } + /** * Create the default validator instance. * diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index 21985e2fb7a1..cf47fb43a1e0 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\Response; +use Illuminate\Config\Repository; use Illuminate\Container\Container; use Illuminate\Contracts\Translation\Translator; use Illuminate\Contracts\Validation\Factory as ValidationFactoryContract; @@ -14,6 +15,8 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Routing\Redirector; use Illuminate\Routing\UrlGenerator; +use Illuminate\Translation\ArrayLoader; +use Illuminate\Translation\Translator as TranslatorConcrete; use Illuminate\Validation\Factory as ValidationFactory; use Illuminate\Validation\ValidationException; use Mockery as m; @@ -25,6 +28,8 @@ class FoundationFormRequestTest extends TestCase protected function tearDown(): void { + Container::setInstance(null); + $this->mocks = []; parent::tearDown(); @@ -241,6 +246,112 @@ public function testRequestWithGetRules() $request->validateResolved(); } + public function testFailOnUnknownFieldsRejectsExtraInputWhenEnabledOnRequest() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'unexpected' => 'value'], + FoundationTestFormRequestStrictStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsAllowsExtraInputWhenExplicitlyDisabledOnRequest() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'with' => 'extras'], + FoundationTestFormRequestStrictDisabledStub::class + ); + + $request->validateResolved(); + + $this->assertEquals(['name' => 'Taylor'], $request->validated()); + } + + public function testFailOnUnknownFieldsEnabledViaApplicationConfig() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'unexpected' => 'value'], + FoundationTestFormRequestStub::class, + [ + 'validation' => [ + 'fail_on_unknown_fields' => true, + ], + ] + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsPropertyOverridesConfig() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'with' => 'extras'], + FoundationTestFormRequestStrictDisabledStub::class, + [ + 'validation' => [ + 'fail_on_unknown_fields' => true, + ], + ] + ); + + $request->validateResolved(); + + $this->assertEquals(['name' => 'Taylor'], $request->validated()); + } + + public function testFailOnUnknownFieldsAllowsKeysMatchingWildcardRules() + { + $request = $this->createRequest( + [ + 'items' => [ + ['id' => 1, 'name' => 'a'], + ['id' => 2, 'name' => 'b'], + ], + ], + FoundationTestFormRequestStrictWildcardStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('items.0.name')); + } + + public function testFailOnUnknownFieldsPassesForInputMatchingWildcardRulesOnly() + { + $request = $this->createRequest( + [ + 'items' => [ + ['id' => 1], + ['id' => 2], + ], + ], + FoundationTestFormRequestStrictWildcardStub::class + ); + + $request->validateResolved(); + + $this->assertSame( + [ + 'items' => [ + ['id' => 1], + ['id' => 2], + ], + ], + $request->validated() + ); + } + /** * Catch the given exception thrown from the executor, and return it. * @@ -270,17 +381,30 @@ protected function catchException($class, $executor) * * @param array $payload * @param string $class + * @param array $config * @return \Illuminate\Foundation\Http\FormRequest */ - protected function createRequest($payload = [], $class = FoundationTestFormRequestStub::class) + protected function createRequest($payload = [], $class = FoundationTestFormRequestStub::class, array $config = []) { - $container = tap(new Container, function ($container) { + $container = tap(new Container, function ($container) use ($config) { $container->instance( ValidationFactoryContract::class, $this->createValidationFactory($container) ); + + $container->instance('translator', new TranslatorConcrete(new ArrayLoader([ + 'validation' => [ + 'prohibited' => 'The :attribute field is prohibited.', + ], + ]), 'en')); + + if ($config !== []) { + $container->instance('config', new Repository($config)); + } }); + Container::setInstance($container); + $request = $class::create('/', 'GET', $payload); return $request->setRedirector($this->createMockRedirector($request)) @@ -296,6 +420,7 @@ protected function createRequest($payload = [], $class = FoundationTestFormReque protected function createValidationFactory($container) { $translator = m::mock(Translator::class)->shouldReceive('get') + ->zeroOrMoreTimes()->andReturn('error')->shouldReceive('choice') ->zeroOrMoreTimes()->andReturn('error')->getMock(); return new ValidationFactory($translator, $container); @@ -542,3 +667,48 @@ protected function validationRules(): array } } } + +class FoundationTestFormRequestStrictStub extends FormRequest +{ + protected ?bool $failOnUnknownFields = true; + + public function rules() + { + return ['name' => 'required']; + } + + public function authorize() + { + return true; + } +} + +class FoundationTestFormRequestStrictDisabledStub extends FormRequest +{ + protected ?bool $failOnUnknownFields = false; + + public function rules() + { + return ['name' => 'required']; + } + + public function authorize() + { + return true; + } +} + +class FoundationTestFormRequestStrictWildcardStub extends FormRequest +{ + protected ?bool $failOnUnknownFields = true; + + public function rules() + { + return ['items.*.id' => 'required']; + } + + public function authorize() + { + return true; + } +} From 25205387331fabaa78d6741db09f2bc6791cdb3f Mon Sep 17 00:00:00 2001 From: NurullahDemirel Date: Sun, 29 Mar 2026 13:40:56 +0300 Subject: [PATCH 2/6] delete validation.php file --- config/validation.php | 5 ---- .../Foundation/Http/FormRequest.php | 17 ++++--------- .../Foundation/FoundationFormRequestTest.php | 24 ++++++++----------- 3 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 config/validation.php diff --git a/config/validation.php b/config/validation.php deleted file mode 100644 index ae8f45374065..000000000000 --- a/config/validation.php +++ /dev/null @@ -1,5 +0,0 @@ - false, -]; diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 826d644de059..e5f5f9932534 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -74,7 +74,7 @@ class FormRequest extends Request implements ValidatesWhenResolved /** * Per-class override for rejecting fields not defined in rules(). * - * Null falls through to the global flag or the config value. + * Null falls through to the global flag set via FormRequest::failOnUnknownFields(). * * @var bool|null */ @@ -187,9 +187,8 @@ protected function configureFromAttributes() * Determine if fields not present in rules() should fail validation. * * Resolution order: - * 1. $failOnUnknownFields → per-class override - * 2. $globalFailOnUnknownFields → FormRequest::failOnUnknownFields() ile set edilen global değer - * 3. Config "validation.fail_on_unknown_fields" + * 1. $failOnUnknownFields — per-class override + * 2. $globalFailOnUnknownFields — set via FormRequest::failOnUnknownFields() * * @return bool */ @@ -199,15 +198,7 @@ protected function shouldFailOnUnknownFields(): bool return $this->failOnUnknownFields; } - if (static::$globalFailOnUnknownFields) { - return true; - } - - if ($this->container->bound('config')) { - return (bool) $this->container->make('config')->get('validation.fail_on_unknown_fields', false); - } - - return false; + return static::$globalFailOnUnknownFields; } /** diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index cf47fb43a1e0..ee04610a253a 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -28,6 +28,8 @@ class FoundationFormRequestTest extends TestCase protected function tearDown(): void { + FormRequest::failOnUnknownFields(false); + Container::setInstance(null); $this->mocks = []; @@ -272,16 +274,13 @@ public function testFailOnUnknownFieldsAllowsExtraInputWhenExplicitlyDisabledOnR $this->assertEquals(['name' => 'Taylor'], $request->validated()); } - public function testFailOnUnknownFieldsEnabledViaApplicationConfig() + public function testFailOnUnknownFieldsEnabledViaFailOnUnknownFieldsStaticMethod() { + FormRequest::failOnUnknownFields(); + $request = $this->createRequest( ['name' => 'Taylor', 'unexpected' => 'value'], - FoundationTestFormRequestStub::class, - [ - 'validation' => [ - 'fail_on_unknown_fields' => true, - ], - ] + FoundationTestFormRequestStub::class ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -291,16 +290,13 @@ public function testFailOnUnknownFieldsEnabledViaApplicationConfig() $this->assertTrue($exception->validator->errors()->has('unexpected')); } - public function testFailOnUnknownFieldsPropertyOverridesConfig() + public function testFailOnUnknownFieldsPropertyOverridesGlobalStatic() { + FormRequest::failOnUnknownFields(); + $request = $this->createRequest( ['name' => 'Taylor', 'with' => 'extras'], - FoundationTestFormRequestStrictDisabledStub::class, - [ - 'validation' => [ - 'fail_on_unknown_fields' => true, - ], - ] + FoundationTestFormRequestStrictDisabledStub::class ); $request->validateResolved(); From 0c67dce1614fbdeddb166dd13840f7f80f243dc9 Mon Sep 17 00:00:00 2001 From: NurullahDemirel Date: Sun, 29 Mar 2026 13:53:02 +0300 Subject: [PATCH 3/6] formated --- src/Illuminate/Foundation/Http/FormRequest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index e5f5f9932534..ca5e54725c07 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -235,10 +235,10 @@ protected function isAllowedKey(string $inputKey, array $allowedKeys): bool if (str_contains($ruleKey, '*')) { $pattern = '/^'.str_replace( - ['\*', '\.'], - ['[^.]+', '\.'], - preg_quote($ruleKey, '/') - ).'$/'; + ['\*', '\.'], + ['[^.]+', '\.'], + preg_quote($ruleKey, '/') + ).'$/'; if (preg_match($pattern, $inputKey)) { return true; From 3de91b220cad4d3fd30d182bbffd197ab93a4991 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 6 Apr 2026 08:25:25 -0500 Subject: [PATCH 4/6] formatting --- .../Http/Attributes/FailOnUnknownFields.php | 13 +++ .../Foundation/Http/FormRequest.php | 50 +++-------- .../Foundation/FoundationFormRequestTest.php | 86 ++++++++++++++----- 3 files changed, 88 insertions(+), 61 deletions(-) create mode 100644 src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php diff --git a/src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php b/src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php new file mode 100644 index 000000000000..2f85d0660b05 --- /dev/null +++ b/src/Illuminate/Foundation/Http/Attributes/FailOnUnknownFields.php @@ -0,0 +1,13 @@ +isProduction()); - * * @param bool $value * @return void */ @@ -181,21 +170,20 @@ protected function configureFromAttributes() if (count($errorBag) > 0) { $this->errorBag = $errorBag[0]->newInstance()->name; } + } /** * Determine if fields not present in rules() should fail validation. * - * Resolution order: - * 1. $failOnUnknownFields — per-class override - * 2. $globalFailOnUnknownFields — set via FormRequest::failOnUnknownFields() - * * @return bool */ protected function shouldFailOnUnknownFields(): bool { - if ($this->failOnUnknownFields !== null) { - return $this->failOnUnknownFields; + $failOnUnknownFields = (new ReflectionClass($this))->getAttributes(FailOnUnknownFields::class); + + if ($failOnUnknownFields !== []) { + return $failOnUnknownFields[0]->newInstance()->value; } return static::$globalFailOnUnknownFields; @@ -207,16 +195,15 @@ protected function shouldFailOnUnknownFields(): bool */ protected function validateNoExtraFields(Validator $validator): void { - $allowedKeys = array_keys($this->container->call([$this, 'rules'])); + $allowedKeys = array_keys($this->validationRules()); $inputKeys = array_keys(Arr::dot($this->all())); foreach ($inputKeys as $inputKey) { if (! $this->isAllowedKey($inputKey, $allowedKeys)) { - $validator->errors()->add( - $inputKey, - $this->getFailOnUnknownFieldsMessage($inputKey) - ); + $validator->errors()->add($inputKey, trans('validation.prohibited', [ + 'attribute' => str_replace('_', ' ', $inputKey), + ])); } } } @@ -234,31 +221,18 @@ protected function isAllowedKey(string $inputKey, array $allowedKeys): bool } if (str_contains($ruleKey, '*')) { - $pattern = '/^'.str_replace( - ['\*', '\.'], - ['[^.]+', '\.'], - preg_quote($ruleKey, '/') - ).'$/'; + $pattern = '/^'.str_replace('\*', '[^.]+', preg_quote($ruleKey, '/')).'$/'; if (preg_match($pattern, $inputKey)) { return true; } } + } return false; } - /** - * @param string $field The name of the unexpected field. - * @return string - */ - protected function getFailOnUnknownFieldsMessage(string $field): string - { - return trans('validation.prohibited', ['attribute' => str_replace('_', ' ', $field)]) - ?: "The {$field} field is not allowed."; - } - /** * Create the default validator instance. * diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index ee04610a253a..a15d725b9b68 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -5,12 +5,12 @@ use Exception; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\Response; -use Illuminate\Config\Repository; use Illuminate\Container\Container; use Illuminate\Contracts\Translation\Translator; use Illuminate\Contracts\Validation\Factory as ValidationFactoryContract; use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\Attributes\ErrorBag; +use Illuminate\Foundation\Http\Attributes\FailOnUnknownFields; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\RedirectResponse; use Illuminate\Routing\Redirector; @@ -252,7 +252,7 @@ public function testFailOnUnknownFieldsRejectsExtraInputWhenEnabledOnRequest() { $request = $this->createRequest( ['name' => 'Taylor', 'unexpected' => 'value'], - FoundationTestFormRequestStrictStub::class + FoundationTestFormRequestFailOnUnknownFieldsStub::class ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -266,7 +266,7 @@ public function testFailOnUnknownFieldsAllowsExtraInputWhenExplicitlyDisabledOnR { $request = $this->createRequest( ['name' => 'Taylor', 'with' => 'extras'], - FoundationTestFormRequestStrictDisabledStub::class + FoundationTestFormRequestSkipUnknownFieldsFailureStub::class ); $request->validateResolved(); @@ -290,13 +290,29 @@ public function testFailOnUnknownFieldsEnabledViaFailOnUnknownFieldsStaticMethod $this->assertTrue($exception->validator->errors()->has('unexpected')); } - public function testFailOnUnknownFieldsPropertyOverridesGlobalStatic() + public function testFailOnUnknownFieldsWorksWhenRequestDoesNotDefineRulesMethod() + { + FormRequest::failOnUnknownFields(); + + $request = $this->createRequest( + ['unexpected' => 'value'], + FoundationTestFormRequestWithoutRulesMethod::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsAttributeOverridesGlobalStatic() { FormRequest::failOnUnknownFields(); $request = $this->createRequest( ['name' => 'Taylor', 'with' => 'extras'], - FoundationTestFormRequestStrictDisabledStub::class + FoundationTestFormRequestSkipUnknownFieldsFailureStub::class ); $request->validateResolved(); @@ -313,7 +329,7 @@ public function testFailOnUnknownFieldsAllowsKeysMatchingWildcardRules() ['id' => 2, 'name' => 'b'], ], ], - FoundationTestFormRequestStrictWildcardStub::class + FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class ); $exception = $this->catchException(ValidationException::class, function () use ($request) { @@ -332,7 +348,7 @@ public function testFailOnUnknownFieldsPassesForInputMatchingWildcardRulesOnly() ['id' => 2], ], ], - FoundationTestFormRequestStrictWildcardStub::class + FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub::class ); $request->validateResolved(); @@ -348,6 +364,24 @@ public function testFailOnUnknownFieldsPassesForInputMatchingWildcardRulesOnly() ); } + public function testFailOnUnknownFieldsWildcardMatchesSingleSegmentOnly() + { + $request = $this->createRequest( + [ + 'items' => [ + ['name' => 'a'], + ], + ], + FoundationTestFormRequestFailOnUnknownFieldsSingleSegmentWildcardStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('items.0.name')); + } + /** * Catch the given exception thrown from the executor, and return it. * @@ -377,12 +411,11 @@ protected function catchException($class, $executor) * * @param array $payload * @param string $class - * @param array $config * @return \Illuminate\Foundation\Http\FormRequest */ - protected function createRequest($payload = [], $class = FoundationTestFormRequestStub::class, array $config = []) + protected function createRequest($payload = [], $class = FoundationTestFormRequestStub::class) { - $container = tap(new Container, function ($container) use ($config) { + $container = tap(new Container, function ($container) { $container->instance( ValidationFactoryContract::class, $this->createValidationFactory($container) @@ -393,10 +426,6 @@ protected function createRequest($payload = [], $class = FoundationTestFormReque 'prohibited' => 'The :attribute field is prohibited.', ], ]), 'en')); - - if ($config !== []) { - $container->instance('config', new Repository($config)); - } }); Container::setInstance($container); @@ -664,10 +693,9 @@ protected function validationRules(): array } } -class FoundationTestFormRequestStrictStub extends FormRequest +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsStub extends FormRequest { - protected ?bool $failOnUnknownFields = true; - public function rules() { return ['name' => 'required']; @@ -679,10 +707,9 @@ public function authorize() } } -class FoundationTestFormRequestStrictDisabledStub extends FormRequest +#[FailOnUnknownFields(false)] +class FoundationTestFormRequestSkipUnknownFieldsFailureStub extends FormRequest { - protected ?bool $failOnUnknownFields = false; - public function rules() { return ['name' => 'required']; @@ -694,10 +721,9 @@ public function authorize() } } -class FoundationTestFormRequestStrictWildcardStub extends FormRequest +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsWithWildcardStub extends FormRequest { - protected ?bool $failOnUnknownFields = true; - public function rules() { return ['items.*.id' => 'required']; @@ -708,3 +734,17 @@ public function authorize() return true; } } + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsSingleSegmentWildcardStub extends FormRequest +{ + public function rules() + { + return ['items.*' => 'array']; + } + + public function authorize() + { + return true; + } +} From 9481e12376199bb3546b459734d030aa59a1f84d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 6 Apr 2026 08:31:33 -0500 Subject: [PATCH 5/6] formatting --- .../Foundation/Http/FormRequest.php | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 39233ecd4047..b7a6e7ff7a64 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -72,13 +72,6 @@ class FormRequest extends Request implements ValidatesWhenResolved */ protected $stopOnFirstFailure = false; - /** - * Global flag set via FormRequest::failOnUnknownFields(). - * - * @var bool - */ - protected static bool $globalFailOnUnknownFields = false; - /** * The validator instance. * @@ -87,15 +80,11 @@ class FormRequest extends Request implements ValidatesWhenResolved protected $validator; /** - * Enable or disable unknown-field rejection globally for all form requests. + * Indicates if unknown fields should be rejected for all form requests. * - * @param bool $value - * @return void + * @var bool */ - public static function failOnUnknownFields(bool $value = true): void - { - static::$globalFailOnUnknownFields = $value; - } + protected static bool $globalFailOnUnknownFields = false; /** * Get the validator instance for the request. @@ -131,7 +120,7 @@ protected function getValidatorInstance() if ($this->shouldFailOnUnknownFields()) { $validator->after(function (Validator $validator) { - $this->validateNoExtraFields($validator); + $this->validateNoUnknownFields($validator); }); } @@ -173,66 +162,6 @@ protected function configureFromAttributes() } - /** - * Determine if fields not present in rules() should fail validation. - * - * @return bool - */ - protected function shouldFailOnUnknownFields(): bool - { - $failOnUnknownFields = (new ReflectionClass($this))->getAttributes(FailOnUnknownFields::class); - - if ($failOnUnknownFields !== []) { - return $failOnUnknownFields[0]->newInstance()->value; - } - - return static::$globalFailOnUnknownFields; - } - - /** - * @param \Illuminate\Contracts\Validation\Validator $validator - * @return void - */ - protected function validateNoExtraFields(Validator $validator): void - { - $allowedKeys = array_keys($this->validationRules()); - - $inputKeys = array_keys(Arr::dot($this->all())); - - foreach ($inputKeys as $inputKey) { - if (! $this->isAllowedKey($inputKey, $allowedKeys)) { - $validator->errors()->add($inputKey, trans('validation.prohibited', [ - 'attribute' => str_replace('_', ' ', $inputKey), - ])); - } - } - } - - /** - * @param string $inputKey The dot-notation key from the request input. - * @param array $allowedKeys The keys defined in rules(). - * @return bool - */ - protected function isAllowedKey(string $inputKey, array $allowedKeys): bool - { - foreach ($allowedKeys as $ruleKey) { - if ($ruleKey === $inputKey) { - return true; - } - - if (str_contains($ruleKey, '*')) { - $pattern = '/^'.str_replace('\*', '[^.]+', preg_quote($ruleKey, '/')).'$/'; - - if (preg_match($pattern, $inputKey)) { - return true; - } - } - - } - - return false; - } - /** * Create the default validator instance. * @@ -279,6 +208,66 @@ protected function validationRules() return method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : []; } + /** + * Determine if fields not present in rules should fail validation. + * + * @return bool + */ + protected function shouldFailOnUnknownFields(): bool + { + $failOnUnknownFields = (new ReflectionClass($this))->getAttributes(FailOnUnknownFields::class); + + return $failOnUnknownFields !== [] + ? $failOnUnknownFields[0]->newInstance()->value + : static::$globalFailOnUnknownFields; + } + + /** + * Validate that no unknown fields were sent as input. + * + * @param \Illuminate\Contracts\Validation\Validator $validator + * @return void + */ + protected function validateNoUnknownFields(Validator $validator): void + { + $allowedKeys = array_keys($this->validationRules()); + + foreach (array_keys(Arr::dot($this->all())) as $inputKey) { + if (! $this->isKnownField($inputKey, $allowedKeys)) { + $validator->errors()->add($inputKey, trans('validation.prohibited', [ + 'attribute' => str_replace('_', ' ', $inputKey), + ])); + } + } + } + + /** + * Determine if the given input key is an allowed key based on the validation rules. + * + * @param string $inputKey + * @param array $allowedKeys + * @return bool + */ + protected function isKnownField(string $inputKey, array $allowedKeys): bool + { + foreach ($allowedKeys as $ruleKey) { + if ($ruleKey === $inputKey) { + return true; + } + + if (str_contains($ruleKey, '*')) { + $pattern = '/^'.str_replace('\*', '[^.]+', preg_quote($ruleKey, '/')).'$/'; + + if (preg_match($pattern, $inputKey)) { + return true; + } + } + + } + + return false; + } + /** * Handle a failed validation attempt. * @@ -391,6 +380,17 @@ public function attributes() return []; } + /** + * Enable or disable unknown-field rejection globally for all form requests. + * + * @param bool $value + * @return void + */ + public static function failOnUnknownFields(bool $value = true): void + { + static::$globalFailOnUnknownFields = $value; + } + /** * Set the Validator instance. * From 74694ac89fc5611ed393db54d173e8d03f341c00 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 6 Apr 2026 08:34:30 -0500 Subject: [PATCH 6/6] more tests --- .../Foundation/FoundationFormRequestTest.php | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tests/Foundation/FoundationFormRequestTest.php b/tests/Foundation/FoundationFormRequestTest.php index a15d725b9b68..43d4251ce866 100644 --- a/tests/Foundation/FoundationFormRequestTest.php +++ b/tests/Foundation/FoundationFormRequestTest.php @@ -382,6 +382,79 @@ public function testFailOnUnknownFieldsWildcardMatchesSingleSegmentOnly() $this->assertTrue($exception->validator->errors()->has('items.0.name')); } + public function testFailOnUnknownFieldsRejectsMultipleUnknownKeys() + { + $request = $this->createRequest( + [ + 'name' => 'Taylor', + 'role' => 'admin', + 'profile' => ['is_admin' => true], + ], + FoundationTestFormRequestFailOnUnknownFieldsStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('role')); + $this->assertTrue($exception->validator->errors()->has('profile.is_admin')); + } + + public function testFailOnUnknownFieldsRejectsUnknownNestedSibling() + { + $request = $this->createRequest( + ['user' => ['name' => 'Taylor', 'role' => 'admin']], + FoundationTestFormRequestFailOnUnknownFieldsNestedStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('user.role')); + } + + public function testFailOnUnknownFieldsUsesPreparedInput() + { + $request = $this->createRequest( + ['full_name' => 'Taylor'], + FoundationTestFormRequestFailOnUnknownFieldsPrepareForValidationStub::class + ); + + $request->validateResolved(); + + $this->assertSame(['name' => 'Taylor'], $request->validated()); + } + + public function testFailOnUnknownFieldsChecksRequestPayloadWhenValidationDataIsOverridden() + { + $request = $this->createRequest( + ['name' => 'Taylor', 'unexpected' => 'value'], + FoundationTestFormRequestFailOnUnknownFieldsValidationDataOverrideStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + + public function testFailOnUnknownFieldsStillRunsWithStopOnFirstFailureAttribute() + { + $request = $this->createRequest( + ['unexpected' => 'value'], + FoundationTestFormRequestFailOnUnknownFieldsStopOnFirstFailureStub::class + ); + + $exception = $this->catchException(ValidationException::class, function () use ($request) { + $request->validateResolved(); + }); + + $this->assertTrue($exception->validator->errors()->has('unexpected')); + } + /** * Catch the given exception thrown from the executor, and return it. * @@ -748,3 +821,70 @@ public function authorize() return true; } } + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsNestedStub extends FormRequest +{ + public function rules() + { + return ['user.name' => 'required']; + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsPrepareForValidationStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function prepareForValidation() + { + $this->replace(['name' => $this->input('full_name')]); + } + + public function authorize() + { + return true; + } +} + +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsValidationDataOverrideStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function validationData() + { + return ['name' => $this->input('name')]; + } + + public function authorize() + { + return true; + } +} + +#[StopOnFirstFailure] +#[FailOnUnknownFields] +class FoundationTestFormRequestFailOnUnknownFieldsStopOnFirstFailureStub extends FormRequest +{ + public function rules() + { + return ['name' => 'required']; + } + + public function authorize() + { + return true; + } +}