Skip to content

Commit 7e7d1f0

Browse files
committed
Apply validations for given site urls
1 parent bb8db66 commit 7e7d1f0

File tree

5 files changed

+141
-1
lines changed

5 files changed

+141
-1
lines changed

app/Http/Requests/SiteRequest.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
namespace App\Http\Requests;
44

5+
use App\Rules\RemoteURL;
56
use Illuminate\Foundation\Http\FormRequest;
67

78
class SiteRequest extends FormRequest
89
{
910
public function rules(): array
1011
{
1112
return [
12-
'url' => 'required|url',
13+
'url' => [
14+
'required',
15+
'string',
16+
'url',
17+
'max:255',
18+
new RemoteURL
19+
],
1320
'key' => 'required|string|min:32|max:64',
1421
];
1522
}

app/Network/DNSLookup.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Network;
4+
5+
class DNSLookup
6+
{
7+
public function getIPs($hostname): array
8+
{
9+
// IP as host given
10+
$ips = filter_var($hostname, FILTER_VALIDATE_IP) ? [$hostname] : [];
11+
12+
// Hostname given, resolve IPs
13+
if (count($ips) === 0) {
14+
try {
15+
$dnsResults = dns_get_record($hostname, DNS_A + DNS_AAAA);
16+
} catch (\Throwable $e) {
17+
return [];
18+
}
19+
20+
if ($dnsResults) {
21+
$ips = array_map(function ($dnsResult) {
22+
return !empty($dnsResult['ip']) ? $dnsResult['ip'] : $dnsResult['ipv6'];
23+
}, $dnsResults);
24+
}
25+
}
26+
27+
return $ips;
28+
}
29+
}

app/Rules/RemoteURL.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace App\Rules;
4+
5+
use App\Network\DNSLookup;
6+
use Closure;
7+
use Illuminate\Contracts\Validation\ValidationRule;
8+
use Illuminate\Support\Facades\App;
9+
10+
class RemoteURL implements ValidationRule
11+
{
12+
/**
13+
* Run the validation rule.
14+
*
15+
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
16+
*/
17+
public function validate(string $attribute, mixed $value, Closure $fail): void
18+
{
19+
$host = parse_url($value, PHP_URL_HOST);
20+
$ips = App::make(DNSLookup::class)->getIPs($host);
21+
22+
// Could not resolve given address
23+
if (count($ips) === 0) {
24+
$fail("Invalid URL: unresolvable site URL.");
25+
}
26+
27+
// Check each resolved IP
28+
foreach ($ips as $ip) {
29+
if (!filter_var(
30+
$ip,
31+
FILTER_VALIDATE_IP,
32+
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
33+
)
34+
) {
35+
$fail("Invalid URL: local address are disallowed as site URL.");
36+
}
37+
}
38+
}
39+
}

tests/Unit/Network/DNSLookupTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Tests\Unit\Network;
4+
5+
use App\Network\DNSLookup;
6+
use Tests\TestCase;
7+
8+
class DNSLookupTest extends TestCase
9+
{
10+
public function testIpAsHostIsReturned()
11+
{
12+
$object = new DNSLookup();
13+
$this->assertSame(['127.0.0.1'], $object->getIPs('127.0.0.1'));
14+
}
15+
16+
public function testEmptyArrayIsReturnedForInvalidHost()
17+
{
18+
$object = new DNSLookup();
19+
$this->assertSame([], $object->getIPs('invalid.host.with.bogus.tld'));
20+
}
21+
22+
public function testIpsAreReturned()
23+
{
24+
$object = new DNSLookup();
25+
$this->assertGreaterThan(5, $object->getIPs('joomla.org'));
26+
}
27+
}

tests/Unit/Rules/RemoteURLTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Tests\Unit\Rules;
4+
5+
use App\Rules\RemoteURL;
6+
use PHPUnit\Framework\Attributes\DataProvider;
7+
use Tests\TestCase;
8+
9+
class RemoteURLTest extends TestCase
10+
{
11+
#[DataProvider('urlDataProvider')]
12+
public function testRuleHandlesIpsAndHosts($host, $expectedResult, $expectedMessage)
13+
{
14+
$object = new RemoteURL();
15+
16+
$object->validate('url', $host, function ($message) use ($expectedResult, $expectedMessage) {
17+
if (!$expectedResult) {
18+
$this->assertTrue(true);
19+
$this->assertSame($expectedMessage, $message);
20+
}
21+
});
22+
23+
if ($expectedResult) {
24+
$this->assertTrue(true);
25+
}
26+
}
27+
28+
public static function urlDataProvider(): array
29+
{
30+
return [
31+
['https://127.0.0.1', false, 'Invalid URL: local address are disallowed as site URL.'],
32+
['https://localhost', false, 'Invalid URL: local address are disallowed as site URL.'],
33+
['https://10.0.0.1', false, 'Invalid URL: local address are disallowed as site URL.'],
34+
['https://joomla.org', true, ''],
35+
['https://invalid.host.tld', false,'Invalid URL: unresolvable site URL.'],
36+
];
37+
}
38+
}

0 commit comments

Comments
 (0)