Laravel has a File Validation Bypass
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
12.0.0 → fixed in 12.1.111.0.0 → fixed in 11.44.10 → fixed in 10.48.29
Details
When using wildcard validation to validate a given file or image field array (`files.*`), a user-crafted malicious request could potentially bypass the validation rules.
The fix
[11.x] Add valid values to ensure method (#54840)
src/Illuminate/Collections/Traits/EnumeratesValues.php+1 −1
@@ -342,7 +342,7 @@ public function value($key, $default = null)** @template TEnsureOfType*-* @param class-string<TEnsureOfType>|array<array-key, class-string<TEnsureOfType>> $type+* @param class-string<TEnsureOfType>|array<array-key, class-string<TEnsureOfType>>|scalar|'array'|'null' $type* @return static<TKey, TEnsureOfType>** @throws \UnexpectedValueExceptioncertain rule classes (#54845)src/Illuminate/Validation/Validator.php | 41 +++++++++-----.../Validation/Rules/EmailValidationTest.php | 49 +++++++++++++++++.../Validation/Rules/FileValidationTest.php | 54 +++++++++++++++++++.../Rules/PasswordValidationTest.php | 49 +++++++++++++++++4 files changed, 180 insertions(+), 13 deletions(-)create mode 100644 tests/Integration/Validation/Rules/EmailValidationTest.phpcreate mode 100644 tests/Integration/Validation/Rules/FileValidationTest.phpcreate mode 100644 tests/Integration/Validation/Rules/PasswordValidationTest.php
src/Illuminate/Validation/Validator.php+28 −13
@@ -307,11 +307,11 @@ class Validator implements ValidatorContractprotected $defaultNumericRules = ['Numeric', 'Integer', 'Decimal'];/**-* The current placeholder for dots in rule keys.+* The current random hash for the validator.** @var string*/-protected $dotPlaceholder;+protected static $placeholderHash;/*** The exception to throw upon failure.@@ -344,7 +344,9 @@ public function __construct(array $messages = [],array $attributes = [],) {-$this->dotPlaceholder = Str::random();+if (! isset(static::$placeholderHash)) {+static::$placeholderHash = Str::random();+}$this->initialRules = $rules;$this->translator = $translator;@@ -372,7 +374,7 @@ public function parseData(array $data)$key = str_replace(['.', '*'],-[$this->dotPlaceholder, '__asterisk__'],+['__dot__'.static::$placeholderHash, '__asterisk__'.static::$placeholderHash],$key);@@ -410,7 +412,7 @@ protected function replacePlaceholders($data)protected function replacePlaceholderInString(string $value){return str_replace(-[$this->dotPlaceholder, '__asterisk__'],+['__dot__'.static::$placeholderHash, '__asterisk__'.static::$placeholderHash],['.', '*'],$value);@@ -425,7 +427,7 @@ protected function replacePlaceholderInString(string $value)protected function replaceDotPlaceholderInParameters(array $parameters){return array_map(function ($field) {-return str_replace($this->dotPlaceholder, '.', $field);+return str_replace('__dot__'.static::$placeholderHash, '.', $field);}, $parameters);}@@ -746,7 +748,7 @@ protected function getPrimaryAttribute($attribute)protected function replaceDotInParameters(array $parameters){return array_map(function ($field) {-return str_replace('\.', $this->dotPlaceholder, $field);+return str_replace('\.', '__dot__'.static::$placeholderHash, $field);}, $parameters);}@@ -872,11 +874,24 @@ protected function hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute)*/protected function validateUsingCustomRule($attribute, $value, $rule){-$attribute = $this->replacePlaceholderInString($attribute);+$originalAttribute = $this->replacePlaceholderInString($attribute);++$attribute = match (true) {+$rule instanceof Rules\Email => $attribute,+$rule instanceof Rules\File => $attribute,+$rule instanceof Rules\Password => $attribute,+default => $originalAttribute,+};$value = is_array($value) ? $this->replacePlaceholders($value) : $value;if ($rule instanceof ValidatorAwareRule) {+if ($attribute !== $originalAttribute) {+$this->addCustomAttributes([+$attribute => $this->customAttributes[$originalAttribute] ?? $originalAttribute,+]);+}+$rule->setValidator($this);}@@ -889,14 +904,14 @@ protected function validateUsingCustomRule($attribute, $value, $rule)get_class($rule->invokable()) :get_class($rule);-$this->failedRules[$attribute][$ruleClass] = [];+$this->failedRules[$originalAttribute][$ruleClass] = [];-$messages = $this->getFromLocalArray($attribute, $ruleClass) ?? $rule->message();+$messages = $this->getFromLocalArray($originalAttribute, $ruleClass) ?? $rule->message();$messages = $messages ? (array) $messages : [$ruleClass];foreach ($messages as $key => $message) {-$key = is_string($key) ? $key : $attribute;+$key = is_string($key) ? $key : $originalAttribute;$this->messages->add($key, $this->makeReplacements($message, $key, $ruleClass, []@@ -1189,7 +1204,7 @@ public function getRulesWithoutPlaceholders(){return (new Collection($this->rules))->mapWithKeys(fn ($value, $key) => [-str_replace($this->dotPlaceholder, '\\.', $key) => $value,+str_replace('__dot__'.static::$placeholderHash, '\\.', $key) => $value,])->all();}@@ -1203,7 +1218,7 @@ public function getRulesWithoutPlaceholders()public function setRules(array $rules){$rules = (new Collection($rules))->mapWithKeys(function ($value, $key) {-return [str_replace('\.', $this->dotPlaceholder, $key) => $value];+return [str_replace('\.', '__dot__'.static::$placeholderHash, $key) => $value];})->toArray();$this->initialRules = $rules;
tests/Integration/Validation/Rules/EmailValidationTest.php+49 −0
@@ -0,0 +1,49 @@+<?php++namespace Illuminate\Tests\Integration\Validation\Rules;++use Illuminate\Support\Facades\Validator;+use Illuminate\Validation\Rules\Email;+use Orchestra\Testbench\TestCase;+use PHPUnit\Framework\Attributes\TestWith;++class EmailValidationTest extends TestCase+{+#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array(string $attribute)+{+$validator = Validator::make([+'emails' => [+$attribute => 'taylor@laravel.com',+],+], [+'emails.*' => ['required', Email::default()->rfcCompliant()],+]);++$this->assertTrue($validator->passes());+}++#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute)+{+$validator = Validator::make([+'emails' => [+$attribute => 'taylor[at]laravel.com',+],+], [+'emails.*' => ['required', Email::default()->rfcCompliant()],+]);++$this->assertFalse($validator->passes());++$this->assertSame([+0 => __('validation.email', ['attribute' => sprintf('emails.%s', str_replace('_', ' ', $attribute))]),+], $validator->messages()->all());+}+}
tests/Integration/Validation/Rules/FileValidationTest.php+54 −0
@@ -0,0 +1,54 @@+<?php++namespace Illuminate\Tests\Integration\Validation\Rules;++use Illuminate\Http\UploadedFile;+use Illuminate\Support\Facades\Validator;+use Illuminate\Validation\Rules\File;+use Orchestra\Testbench\TestCase;+use PHPUnit\Framework\Attributes\TestWith;++class FileValidationTest extends TestCase+{+#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array(string $attribute)+{+$file = UploadedFile::fake()->create('laravel.png', 1, 'image/png');++$validator = Validator::make([+'files' => [+$attribute => $file,+],+], [+'files.*' => ['required', File::types(['image/png', 'image/jpeg'])],+]);++$this->assertTrue($validator->passes());+}++#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute)+{+$file = UploadedFile::fake()->create('laravel.php', 1, 'image/php');++$validator = Validator::make([+'files' => [+$attribute => $file,+],+], [+'files.*' => ['required', File::types($mimes = ['image/png', 'image/jpeg'])],+]);++$this->assertFalse($validator->passes());++$this->assertSame([+0 => __('validation.mimetypes', ['attribute' => sprintf('files.%s', str_replace('_', ' ', $attribute)), 'values' => implode(', ', $mimes)]),+], $validator->messages()->all());+}+}
tests/Integration/Validation/Rules/PasswordValidationTest.php+49 −0
@@ -0,0 +1,49 @@+<?php++namespace Illuminate\Tests\Integration\Validation\Rules;++use Illuminate\Support\Facades\Validator;+use Illuminate\Validation\Rules\Password;+use Orchestra\Testbench\TestCase;+use PHPUnit\Framework\Attributes\TestWith;++class PasswordValidationTest extends TestCase+{+#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array(string $attribute)+{+$validator = Validator::make([+'passwords' => [+$attribute => 'secret',+],+], [+'passwords.*' => ['required', Password::default()->min(6)],+]);++$this->assertTrue($validator->passes());+}++#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute)+{+$validator = Validator::make([+'passwords' => [+$attribute => 'secret',+],+], [+'passwords.*' => ['required', Password::default()->min(8)],+]);++$this->assertFalse($validator->passes());++$this->assertSame([+0 => sprintf('The passwords.%s field must be at least 8 characters.', str_replace('_', ' ', $attribute)),+], $validator->messages()->all());+}+}resolve application when project name is "vendor" (#54871)src/Illuminate/Foundation/Application.php | 2 +-1 file changed, 1 insertion(+), 1 deletion(-)
src/Illuminate/Foundation/Application.php+1 −1
@@ -257,7 +257,7 @@ public static function inferBasePath()isset($_ENV['APP_BASE_PATH']) => $_ENV['APP_BASE_PATH'],default => dirname(array_values(array_filter(array_keys(ClassLoader::getRegisteredLoaders()),-fn ($path) => ! str_contains($path, '/vendor/'),+fn ($path) => ! str_starts_with($path, 'phar://'),))[0]),};}composer.json | 2 +-tests/Integration/Cache/DynamoDbStoreTest.php | 2 +-tests/Integration/Cookie/CookieTest.php | 2 +-...ctionWithAfterCommitUsingDatabaseTransactionsTest.php | 2 +-.../DatabaseEmulatePreparesMariaDbConnectionTest.php | 4 ++--.../MySql/DatabaseEmulatePreparesMySqlConnectionTest.php | 4 ++--.../Database/Postgres/PostgresSchemaBuilderTest.php | 4 ++--tests/Integration/Mail/SendingMarkdownMailTest.php | 2 +-tests/Integration/Mail/SendingQueuedMailTest.php | 2 +-tests/Integration/Queue/JobEncryptionTest.php | 4 ++--tests/Integration/Queue/ModelSerializationTest.php | 2 +-tests/Integration/Queue/QueueConnectionTest.php | 9 +++------tests/Integration/View/BladeAnonymousComponentTest.php | 2 +-tests/Integration/View/BladeTest.php | 2 +-tests/Integration/View/RenderableViewExceptionTest.php | 2 +-15 files changed, 21 insertions(+), 24 deletions(-)
composer.json+1 −1
@@ -111,7 +111,7 @@"league/flysystem-read-only": "^3.25.1","league/flysystem-sftp-v3": "^3.25.1","mockery/mockery": "^1.6.10",-"orchestra/testbench-core": "^9.9.4",+"orchestra/testbench-core": "^9.11.2","pda/pheanstalk": "^5.0.6","php-http/discovery": "^1.15","phpstan/phpstan": "^2.0",
tests/Integration/Cache/DynamoDbStoreTest.php+1 −1
@@ -65,7 +65,7 @@ public function testLocksCanBeAcquired()* @param \Illuminate\Foundation\Application $app* @return void*/-protected function getEnvironmentSetUp($app)+protected function defineEnvironment($app){if (! env('DYNAMODB_CACHE_TABLE')) {$this->markTestSkipped('DynamoDB not configured.');
More files changed — see the full commit.
[10.x] Fix attribute name used on `Validator` instance within
src/Illuminate/Validation/Validator.php+26 −12
@@ -302,11 +302,11 @@ class Validator implements ValidatorContractprotected $defaultNumericRules = ['Numeric', 'Integer', 'Decimal'];/**-* The current placeholder for dots in rule keys.+* The current random hash for the validator.** @var string*/-protected $dotPlaceholder;+protected static $placeholderHash;/*** The exception to throw upon failure.@@ -335,7 +335,9 @@ class Validator implements ValidatorContractpublic function __construct(Translator $translator, array $data, array $rules,array $messages = [], array $attributes = []){-$this->dotPlaceholder = Str::random();+if (! isset(static::$placeholderHash)) {+static::$placeholderHash = Str::random();+}$this->initialRules = $rules;$this->translator = $translator;@@ -363,7 +365,7 @@ public function parseData(array $data)$key = str_replace(['.', '*'],-[$this->dotPlaceholder, '__asterisk__'],+['__dot__'.static::$placeholderHash, '__asterisk__'.static::$placeholderHash],$key);@@ -401,7 +403,7 @@ protected function replacePlaceholders($data)protected function replacePlaceholderInString(string $value){return str_replace(-[$this->dotPlaceholder, '__asterisk__'],+['__dot__'.static::$placeholderHash, '__asterisk__'.static::$placeholderHash],['.', '*'],$value);@@ -720,7 +722,7 @@ protected function getPrimaryAttribute($attribute)protected function replaceDotInParameters(array $parameters){return array_map(function ($field) {-return str_replace('\.', $this->dotPlaceholder, $field);+return str_replace('\.', '__dot__'.static::$placeholderHash, $field);}, $parameters);}@@ -846,11 +848,23 @@ protected function hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute)*/protected function validateUsingCustomRule($attribute, $value, $rule){-$attribute = $this->replacePlaceholderInString($attribute);+$originalAttribute = $this->replacePlaceholderInString($attribute);++$attribute = match (true) {+$rule instanceof Rules\File => $attribute,+$rule instanceof Rules\Password => $attribute,+default => $originalAttribute,+};$value = is_array($value) ? $this->replacePlaceholders($value) : $value;if ($rule instanceof ValidatorAwareRule) {+if ($attribute !== $originalAttribute) {+$this->addCustomAttributes([+$attribute => $this->customAttributes[$originalAttribute] ?? $originalAttribute,+]);+}+$rule->setValidator($this);}@@ -863,14 +877,14 @@ protected function validateUsingCustomRule($attribute, $value, $rule)get_class($rule->invokable()) :get_class($rule);-$this->failedRules[$attribute][$ruleClass] = [];+$this->failedRules[$originalAttribute][$ruleClass] = [];-$messages = $this->getFromLocalArray($attribute, $ruleClass) ?? $rule->message();+$messages = $this->getFromLocalArray($originalAttribute, $ruleClass) ?? $rule->message();$messages = $messages ? (array) $messages : [$ruleClass];foreach ($messages as $key => $message) {-$key = is_string($key) ? $key : $attribute;+$key = is_string($key) ? $key : $originalAttribute;$this->messages->add($key, $this->makeReplacements($message, $key, $ruleClass, []@@ -1159,7 +1173,7 @@ public function getRulesWithoutPlaceholders(){return collect($this->rules)->mapWithKeys(fn ($value, $key) => [-str_replace($this->dotPlaceholder, '\\.', $key) => $value,+str_replace('__dot__'.static::$placeholderHash, '\\.', $key) => $value,])->all();}@@ -1173,7 +1187,7 @@ public function getRulesWithoutPlaceholders()public function setRules(array $rules){$rules = collect($rules)->mapWithKeys(function ($value, $key) {-return [str_replace('\.', $this->dotPlaceholder, $key) => $value];+return [str_replace('\.', '__dot__'.static::$placeholderHash, $key) => $value];})->toArray();$this->initialRules = $rules;
tests/Integration/Validation/Rules/FileValidationTest.php+54 −0
@@ -0,0 +1,54 @@+<?php++namespace Illuminate\Tests\Integration\Validation\Rules;++use Illuminate\Http\UploadedFile;+use Illuminate\Support\Facades\Validator;+use Illuminate\Validation\Rules\File;+use Orchestra\Testbench\TestCase;+use PHPUnit\Framework\Attributes\TestWith;++class FileValidationTest extends TestCase+{+#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array(string $attribute)+{+$file = UploadedFile::fake()->create('laravel.png', 1, 'image/png');++$validator = Validator::make([+'files' => [+$attribute => $file,+],+], [+'files.*' => ['required', File::types(['image/png', 'image/jpeg'])],+]);++$this->assertTrue($validator->passes());+}++#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute)+{+$file = UploadedFile::fake()->create('laravel.php', 1, 'image/php');++$validator = Validator::make([+'files' => [+$attribute => $file,+],+], [+'files.*' => ['required', File::types($mimes = ['image/png', 'image/jpeg'])],+]);++$this->assertFalse($validator->passes());++$this->assertSame([+0 => __('validation.mimetypes', ['attribute' => sprintf('files.%s', str_replace('_', ' ', $attribute)), 'values' => implode(', ', $mimes)]),+], $validator->messages()->all());+}+}
tests/Integration/Validation/Rules/PasswordValidationTest.php+49 −0
@@ -0,0 +1,49 @@+<?php++namespace Illuminate\Tests\Integration\Validation\Rules;++use Illuminate\Support\Facades\Validator;+use Illuminate\Validation\Rules\Password;+use Orchestra\Testbench\TestCase;+use PHPUnit\Framework\Attributes\TestWith;++class PasswordValidationTest extends TestCase+{+#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array(string $attribute)+{+$validator = Validator::make([+'passwords' => [+$attribute => 'secret',+],+], [+'passwords.*' => ['required', Password::default()->min(6)],+]);++$this->assertTrue($validator->passes());+}++#[TestWith(['0'])]+#[TestWith(['.'])]+#[TestWith(['*'])]+#[TestWith(['__asterisk__'])]+public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute)+{+$validator = Validator::make([+'passwords' => [+$attribute => 'secret',+],+], [+'passwords.*' => ['required', Password::default()->min(8)],+]);++$this->assertFalse($validator->passes());++$this->assertSame([+0 => sprintf('The passwords.%s field must be at least 8 characters.', str_replace('_', ' ', $attribute)),+], $validator->messages()->all());+}+}
References
- WEBhttps://github.com/laravel/framework/security/advisories/GHSA-78fx-h6xr-vch4
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2025-27515
- WEBhttps://github.com/laravel/framework/commit/2d133034fefddfb047838f4caca3687a3ba811a5
- WEBhttps://github.com/laravel/framework/commit/a4f7a8f9b83e21882abeef78c3174c66b0f4a26b
- PACKAGEhttps://github.com/laravel/framework