From 71eba1658871c598fcf66f7234735a62a3a11eb1 Mon Sep 17 00:00:00 2001 From: Sjors Ottjes Date: Fri, 24 Jul 2026 16:39:42 +0200 Subject: [PATCH 1/3] [13.x] Add Validator::fakeDnsLookups() --- src/Illuminate/Support/Facades/Validator.php | 1 + .../Concerns/ValidatesAttributes.php | 7 ++- src/Illuminate/Validation/Factory.php | 11 ++++ src/Illuminate/Validation/Validator.php | 19 +++++++ tests/Validation/ValidationFactoryTest.php | 15 +++++ tests/Validation/ValidationValidatorTest.php | 55 +++++++++++++++++++ 6 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Facades/Validator.php b/src/Illuminate/Support/Facades/Validator.php index a032cc53ef4c..8adab5af73f3 100755 --- a/src/Illuminate/Support/Facades/Validator.php +++ b/src/Illuminate/Support/Facades/Validator.php @@ -11,6 +11,7 @@ * @method static void replacer(string $rule, \Closure|string $replacer) * @method static void includeUnvalidatedArrayKeys() * @method static void excludeUnvalidatedArrayKeys() + * @method static void fakeDnsLookups(bool $value = true) * @method static void resolver(\Closure $resolver) * @method static \Illuminate\Contracts\Translation\Translator getTranslator() * @method static \Illuminate\Validation\PresenceVerifierInterface getPresenceVerifier() diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 6c22272cc5c2..cc21717524d5 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -147,6 +147,10 @@ public function validateActiveUrl($attribute, $value) */ protected function getDnsRecords($hostname, $type) { + if (static::$fakeDnsLookups) { + return [['host' => $hostname, 'class' => 'IN', 'ttl' => 60, 'type' => 'A', 'ip' => '127.0.0.1']]; + } + return dns_get_record($hostname, $type); } @@ -959,13 +963,14 @@ public function validateEmail($attribute, $value, $parameters) ->unique() ->map(fn ($validation) => match (true) { $validation === 'strict' => new NoRFCWarningsValidation(), - $validation === 'dns' => new DNSCheckValidation(), + $validation === 'dns' => static::$fakeDnsLookups ? null : new DNSCheckValidation(), $validation === 'spoof' => new SpoofCheckValidation(), $validation === 'filter' => new FilterEmailValidation(), $validation === 'filter_unicode' => FilterEmailValidation::unicode(), is_string($validation) && class_exists($validation) => $this->container->make($validation), default => new RFCValidation(), }) + ->filter() ->values() ->all() ?: [new RFCValidation]; diff --git a/src/Illuminate/Validation/Factory.php b/src/Illuminate/Validation/Factory.php index 8cf5027eb26f..62d229597dc1 100755 --- a/src/Illuminate/Validation/Factory.php +++ b/src/Illuminate/Validation/Factory.php @@ -267,6 +267,17 @@ public function excludeUnvalidatedArrayKeys() $this->excludeUnvalidatedArrayKeys = true; } + /** + * Fake the DNS lookups performed by validation rules so they always succeed. + * + * @param bool $value + * @return void + */ + public function fakeDnsLookups($value = true) + { + Validator::fakeDnsLookups($value); + } + /** * Set the Validator instance resolver. * diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 16d4a0abb93e..0880ddf061b6 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -314,6 +314,13 @@ class Validator implements ValidatorContract */ protected static $placeholderHash; + /** + * Indicates if DNS lookups performed by validation rules should be faked to always succeed. + * + * @var bool + */ + protected static $fakeDnsLookups = false; + /** * The exception to throw upon failure. * @@ -1739,6 +1746,17 @@ protected static function decodeAttributeWithPlaceholder(string $attribute) return str_replace('__dot__'.static::$placeholderHash, '\\.', $attribute); } + /** + * Fake the DNS lookups performed by validation rules so they always succeed. + * + * @param bool $value + * @return void + */ + public static function fakeDnsLookups($value = true) + { + static::$fakeDnsLookups = $value; + } + /** * Flush the validator's global state. * @@ -1747,6 +1765,7 @@ protected static function decodeAttributeWithPlaceholder(string $attribute) public static function flushState() { static::$placeholderHash = null; + static::$fakeDnsLookups = false; } /** diff --git a/tests/Validation/ValidationFactoryTest.php b/tests/Validation/ValidationFactoryTest.php index 2a3e3bca56c7..9061eeeaaf18 100755 --- a/tests/Validation/ValidationFactoryTest.php +++ b/tests/Validation/ValidationFactoryTest.php @@ -9,9 +9,17 @@ use Illuminate\Validation\Validator; use Mockery as m; use PHPUnit\Framework\TestCase; +use ReflectionProperty; class ValidationFactoryTest extends TestCase { + protected function tearDown(): void + { + Validator::flushState(); + + parent::tearDown(); + } + public function testMakeMethodCreatesValidValidator() { $translator = m::mock(TranslatorInterface::class); @@ -146,4 +154,11 @@ public function testSetContainer() $this->assertSame($container, $factory->setContainer($container)->getContainer()); } + + public function testFakeDnsLookupsDelegatesToTheValidator() + { + (new Factory(m::mock(TranslatorInterface::class)))->fakeDnsLookups(); + + $this->assertTrue((new ReflectionProperty(Validator::class, 'fakeDnsLookups'))->getValue()); + } } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 39bb2a6bdf91..3fe9d4636892 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -35,6 +35,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; +use ReflectionProperty; use RuntimeException; use stdClass; use Symfony\Component\HttpFoundation\File\File; @@ -42,6 +43,13 @@ class ValidationValidatorTest extends TestCase { + protected function tearDown(): void + { + Validator::flushState(); + + parent::tearDown(); + } + public function testNestedErrorMessagesAreRetrievedFromLocalArray() { $trans = $this->getIlluminateArrayTranslator(); @@ -5368,6 +5376,53 @@ public static function activeUrlDataProvider() ]; } + public function testValidateActiveUrlWithFakedDnsLookups() + { + Validator::fakeDnsLookups(); + + $trans = $this->getIlluminateArrayTranslator(); + + $v = new Validator($trans, ['x' => 'https://this-domain-does-not-exist.invalid'], ['x' => 'active_url']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'aslsdlks'], ['x' => 'active_url']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => ['not-a-string']], ['x' => 'active_url']); + $this->assertFalse($v->passes()); + } + + public function testValidateEmailWithDnsCheckWithFakedDnsLookups() + { + Validator::fakeDnsLookups(); + + $trans = $this->getIlluminateArrayTranslator(); + + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.invalid'], ['x' => 'email:dns']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.invalid'], ['x' => 'email:rfc,dns']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'not-an-email'], ['x' => 'email:dns']); + $this->assertFalse($v->passes()); + } + + public function testFakedDnsLookupsCanBeToggledAndAreFlushed() + { + $property = new ReflectionProperty(Validator::class, 'fakeDnsLookups'); + + Validator::fakeDnsLookups(); + $this->assertTrue($property->getValue()); + + Validator::fakeDnsLookups(false); + $this->assertFalse($property->getValue()); + + Validator::fakeDnsLookups(); + Validator::flushState(); + $this->assertFalse($property->getValue()); + } + public function testValidateImage() { $trans = $this->getIlluminateArrayTranslator(); From 55e26d376b51ed6ab2a6be2e7e9372969f23b552 Mon Sep 17 00:00:00 2001 From: Sjors Ottjes Date: Fri, 24 Jul 2026 16:45:06 +0200 Subject: [PATCH 2/3] Update ValidationValidatorTest.php --- tests/Validation/ValidationValidatorTest.php | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 3fe9d4636892..a8bc38267623 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -31,7 +31,6 @@ use Illuminate\Validation\Validator; use InvalidArgumentException; use Mockery as m; -use Mockery\MockInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; @@ -5335,18 +5334,10 @@ public static function invalidUrls() #[DataProvider('activeUrlDataProvider')] public function testValidateActiveUrl($data, $outcome) { + Validator::fakeDnsLookups(); + $trans = $this->getIlluminateArrayTranslator(); - $v = m::mock( - new Validator($trans, $data, ['x' => 'active_url']), - function (MockInterface $mock) { - $mock - ->shouldAllowMockingProtectedMethods() - ->shouldReceive('getDnsRecords') - ->withAnyArgs() - ->zeroOrMoreTimes() - ->andReturn(['hit']); - } - ); + $v = new Validator($trans, $data, ['x' => 'active_url']); $this->assertEquals($outcome, $v->passes()); } From dafd587a2c68af93692ac07200a1d330bc1e083c Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 24 Jul 2026 10:26:16 -0500 Subject: [PATCH 3/3] formatting --- .../Concerns/ValidatesAttributes.php | 11 +++++++--- .../Validation/FakeDnsGetRecordWrapper.php | 21 +++++++++++++++++++ tests/Validation/ValidationValidatorTest.php | 16 ++++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 src/Illuminate/Validation/FakeDnsGetRecordWrapper.php diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index cc21717524d5..c774c9699819 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -22,6 +22,7 @@ use Illuminate\Support\Exceptions\MathException; use Illuminate\Support\Facades\Date; use Illuminate\Support\Str; +use Illuminate\Validation\FakeDnsGetRecordWrapper; use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Unique; use Illuminate\Validation\ValidationData; @@ -148,6 +149,12 @@ public function validateActiveUrl($attribute, $value) protected function getDnsRecords($hostname, $type) { if (static::$fakeDnsLookups) { + $hostname = rtrim($hostname, '.'); + + if (filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false || filter_var($hostname, FILTER_VALIDATE_IP) !== false) { + return false; + } + return [['host' => $hostname, 'class' => 'IN', 'ttl' => 60, 'type' => 'A', 'ip' => '127.0.0.1']]; } @@ -963,15 +970,13 @@ public function validateEmail($attribute, $value, $parameters) ->unique() ->map(fn ($validation) => match (true) { $validation === 'strict' => new NoRFCWarningsValidation(), - $validation === 'dns' => static::$fakeDnsLookups ? null : new DNSCheckValidation(), + $validation === 'dns' => new DNSCheckValidation(static::$fakeDnsLookups ? new FakeDnsGetRecordWrapper : null), $validation === 'spoof' => new SpoofCheckValidation(), $validation === 'filter' => new FilterEmailValidation(), $validation === 'filter_unicode' => FilterEmailValidation::unicode(), is_string($validation) && class_exists($validation) => $this->container->make($validation), default => new RFCValidation(), }) - ->filter() - ->values() ->all() ?: [new RFCValidation]; $emailValidator = Container::getInstance()->make(EmailValidator::class); diff --git a/src/Illuminate/Validation/FakeDnsGetRecordWrapper.php b/src/Illuminate/Validation/FakeDnsGetRecordWrapper.php new file mode 100644 index 000000000000..26efbea9d53d --- /dev/null +++ b/src/Illuminate/Validation/FakeDnsGetRecordWrapper.php @@ -0,0 +1,21 @@ + 'A']]); + } +} diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a8bc38267623..d63975b41d23 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -5381,6 +5381,12 @@ public function testValidateActiveUrlWithFakedDnsLookups() $v = new Validator($trans, ['x' => ['not-a-string']], ['x' => 'active_url']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 'http://foo..com'], ['x' => 'active_url']); + $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => 'http://127.0.0.1'], ['x' => 'active_url']); + $this->assertFalse($v->passes()); } public function testValidateEmailWithDnsCheckWithFakedDnsLookups() @@ -5389,14 +5395,20 @@ public function testValidateEmailWithDnsCheckWithFakedDnsLookups() $trans = $this->getIlluminateArrayTranslator(); - $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.invalid'], ['x' => 'email:dns']); + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.com'], ['x' => 'email:dns']); $this->assertTrue($v->passes()); - $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.invalid'], ['x' => 'email:rfc,dns']); + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.com'], ['x' => 'email:rfc,dns']); $this->assertTrue($v->passes()); $v = new Validator($trans, ['x' => 'not-an-email'], ['x' => 'email:dns']); $this->assertFalse($v->passes()); + + $v = new Validator($trans, ['x' => '.invalid@gmail.com'], ['x' => 'email:dns']); + $this->assertTrue($v->passes()); + + $v = new Validator($trans, ['x' => 'taylor@this-domain-does-not-exist.invalid'], ['x' => 'email:dns']); + $this->assertFalse($v->passes()); } public function testFakedDnsLookupsCanBeToggledAndAreFlushed()