From 7e7d1f0741025391554b610f84991b7525557f1b Mon Sep 17 00:00:00 2001 From: David Jardin Date: Thu, 22 May 2025 11:58:56 +0200 Subject: [PATCH 1/6] Apply validations for given site urls --- app/Http/Requests/SiteRequest.php | 9 ++++++- app/Network/DNSLookup.php | 29 +++++++++++++++++++++ app/Rules/RemoteURL.php | 39 ++++++++++++++++++++++++++++ tests/Unit/Network/DNSLookupTest.php | 27 +++++++++++++++++++ tests/Unit/Rules/RemoteURLTest.php | 38 +++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 app/Network/DNSLookup.php create mode 100644 app/Rules/RemoteURL.php create mode 100644 tests/Unit/Network/DNSLookupTest.php create mode 100644 tests/Unit/Rules/RemoteURLTest.php diff --git a/app/Http/Requests/SiteRequest.php b/app/Http/Requests/SiteRequest.php index 76c2b9a..2870b2c 100644 --- a/app/Http/Requests/SiteRequest.php +++ b/app/Http/Requests/SiteRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Rules\RemoteURL; use Illuminate\Foundation\Http\FormRequest; class SiteRequest extends FormRequest @@ -9,7 +10,13 @@ class SiteRequest extends FormRequest public function rules(): array { return [ - 'url' => 'required|url', + 'url' => [ + 'required', + 'string', + 'url', + 'max:255', + new RemoteURL + ], 'key' => 'required|string|min:32|max:64', ]; } diff --git a/app/Network/DNSLookup.php b/app/Network/DNSLookup.php new file mode 100644 index 0000000..29c28d7 --- /dev/null +++ b/app/Network/DNSLookup.php @@ -0,0 +1,29 @@ +getIPs($host); + + // Could not resolve given address + if (count($ips) === 0) { + $fail("Invalid URL: unresolvable site URL."); + } + + // Check each resolved IP + foreach ($ips as $ip) { + if (!filter_var( + $ip, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE + ) + ) { + $fail("Invalid URL: local address are disallowed as site URL."); + } + } + } +} diff --git a/tests/Unit/Network/DNSLookupTest.php b/tests/Unit/Network/DNSLookupTest.php new file mode 100644 index 0000000..88fafe2 --- /dev/null +++ b/tests/Unit/Network/DNSLookupTest.php @@ -0,0 +1,27 @@ +assertSame(['127.0.0.1'], $object->getIPs('127.0.0.1')); + } + + public function testEmptyArrayIsReturnedForInvalidHost() + { + $object = new DNSLookup(); + $this->assertSame([], $object->getIPs('invalid.host.with.bogus.tld')); + } + + public function testIpsAreReturned() + { + $object = new DNSLookup(); + $this->assertGreaterThan(5, $object->getIPs('joomla.org')); + } +} diff --git a/tests/Unit/Rules/RemoteURLTest.php b/tests/Unit/Rules/RemoteURLTest.php new file mode 100644 index 0000000..860ecf6 --- /dev/null +++ b/tests/Unit/Rules/RemoteURLTest.php @@ -0,0 +1,38 @@ +validate('url', $host, function ($message) use ($expectedResult, $expectedMessage) { + if (!$expectedResult) { + $this->assertTrue(true); + $this->assertSame($expectedMessage, $message); + } + }); + + if ($expectedResult) { + $this->assertTrue(true); + } + } + + public static function urlDataProvider(): array + { + return [ + ['https://127.0.0.1', false, 'Invalid URL: local address are disallowed as site URL.'], + ['https://localhost', false, 'Invalid URL: local address are disallowed as site URL.'], + ['https://10.0.0.1', false, 'Invalid URL: local address are disallowed as site URL.'], + ['https://joomla.org', true, ''], + ['https://invalid.host.tld', false,'Invalid URL: unresolvable site URL.'], + ]; + } +} From d6d36b4ed24b1339d0a264938476f3ef27936f89 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Thu, 22 May 2025 12:01:25 +0200 Subject: [PATCH 2/6] cs fix --- app/Network/DNSLookup.php | 2 +- app/Rules/RemoteURL.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Network/DNSLookup.php b/app/Network/DNSLookup.php index 29c28d7..fe8777e 100644 --- a/app/Network/DNSLookup.php +++ b/app/Network/DNSLookup.php @@ -4,7 +4,7 @@ class DNSLookup { - public function getIPs($hostname): array + public function getIPs(string $hostname): array { // IP as host given $ips = filter_var($hostname, FILTER_VALIDATE_IP) ? [$hostname] : []; diff --git a/app/Rules/RemoteURL.php b/app/Rules/RemoteURL.php index 3116ee8..b7a5c29 100644 --- a/app/Rules/RemoteURL.php +++ b/app/Rules/RemoteURL.php @@ -16,6 +16,10 @@ class RemoteURL implements ValidationRule */ public function validate(string $attribute, mixed $value, Closure $fail): void { + if (!is_string($value)) { + $fail("Invalid URL: URL must be a string."); + } + $host = parse_url($value, PHP_URL_HOST); $ips = App::make(DNSLookup::class)->getIPs($host); From 3e63a9165c30b2c786593939c56028cb9de18b81 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Thu, 22 May 2025 12:01:54 +0200 Subject: [PATCH 3/6] fix tests --- tests/Feature/Api/SiteControllerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Api/SiteControllerTest.php b/tests/Feature/Api/SiteControllerTest.php index 3a6efd9..342f77d 100644 --- a/tests/Feature/Api/SiteControllerTest.php +++ b/tests/Feature/Api/SiteControllerTest.php @@ -123,7 +123,7 @@ public function testCheckingASiteReturns404ForInvalidSite(): void ["url" => "https://www.joomlaf.org", "key" => "foobar123foobar123foobar123foobar123"] ); - $response->assertStatus(404); + $response->assertStatus(422); } public function testDeleteASiteReturns404ForInvalidSite(): void @@ -133,7 +133,7 @@ public function testDeleteASiteReturns404ForInvalidSite(): void ["url" => "https://www.joomlaf.org", "key" => "foobar123foobar123foobar123foobar123"] ); - $response->assertStatus(404); + $response->assertStatus(422); } public function testDeleteASiteRemovesRow(): void From 2e5e13845e0c4b1bb6a5630d16c999c974b166fb Mon Sep 17 00:00:00 2001 From: David Jardin Date: Thu, 22 May 2025 12:02:21 +0200 Subject: [PATCH 4/6] cs fix --- app/Http/Requests/SiteRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/SiteRequest.php b/app/Http/Requests/SiteRequest.php index 2870b2c..7027eaf 100644 --- a/app/Http/Requests/SiteRequest.php +++ b/app/Http/Requests/SiteRequest.php @@ -15,7 +15,7 @@ public function rules(): array 'string', 'url', 'max:255', - new RemoteURL + new RemoteURL() ], 'key' => 'required|string|min:32|max:64', ]; From b25e190acd07b797120739b2865cce7f8a2e7883 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Thu, 22 May 2025 12:03:29 +0200 Subject: [PATCH 5/6] fix stan --- app/Rules/RemoteURL.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Rules/RemoteURL.php b/app/Rules/RemoteURL.php index b7a5c29..1afd8a1 100644 --- a/app/Rules/RemoteURL.php +++ b/app/Rules/RemoteURL.php @@ -18,6 +18,8 @@ public function validate(string $attribute, mixed $value, Closure $fail): void { if (!is_string($value)) { $fail("Invalid URL: URL must be a string."); + + return; } $host = parse_url($value, PHP_URL_HOST); From 51823c1b4e903404e6e7ea53e7048debf31467c8 Mon Sep 17 00:00:00 2001 From: David Jardin Date: Thu, 22 May 2025 12:04:45 +0200 Subject: [PATCH 6/6] fix stan --- app/Rules/RemoteURL.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Rules/RemoteURL.php b/app/Rules/RemoteURL.php index 1afd8a1..f879985 100644 --- a/app/Rules/RemoteURL.php +++ b/app/Rules/RemoteURL.php @@ -22,7 +22,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void return; } - $host = parse_url($value, PHP_URL_HOST); + $host = (string) parse_url($value, PHP_URL_HOST); $ips = App::make(DNSLookup::class)->getIPs($host); // Could not resolve given address