Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
14 changes: 13 additions & 1 deletion app/Models/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Models;

use App\Network\NetworkHelper;
use App\RemoteSite\Connection;
use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\Model;
Expand Down Expand Up @@ -38,7 +39,18 @@ protected function casts(): array

public function getUrlAttribute(string $value): string
{
return rtrim($value, "/");
$url = rtrim($value, "/");

$networkHelper = App::make(NetworkHelper::class);

// Revalidate current DNS resolution on every usage
if (!$networkHelper->isValidRemoteHost(
(string) parse_url($value, PHP_URL_HOST)
)) {
throw new \RuntimeException("Invalid URL provided");
}

return $url;
}

public function getConnectionAttribute(): Connection
Expand Down
33 changes: 32 additions & 1 deletion app/Network/DNSLookup.php → app/Network/NetworkHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace App\Network;

class DNSLookup
class NetworkHelper
{
public function getIPs(string $hostname): array
{
Expand All @@ -26,4 +26,35 @@ public function getIPs(string $hostname): array

return $ips;
}

public function isValidRemoteHost(string $hostname): bool
{
$ips = $this->getIPs($hostname);

if (!count($ips)) {
return false;
}

// Check each resolved IP
foreach ($ips as $ip) {
if (!$this->isValidRemoteIp($ip)) {
return false;
}
}

return true;
}

public function isValidRemoteIp(string $ip): bool
{
if (!filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
)) {
return false;
}

return true;
}
}
4 changes: 2 additions & 2 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace App\Providers;

use App\Network\DNSLookup;
use App\Network\NetworkHelper;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
Expand Down Expand Up @@ -34,7 +34,7 @@ public function boot(): void
$siteIpLimits = [];

if ($siteHost !== 'default') {
$siteIps = (new DNSLookup())->getIPs($siteHost);
$siteIps = (new NetworkHelper())->getIPs($siteHost);

foreach ($siteIps as $siteIp) {
$siteIpLimits[] = Limit::perMinute(5)->by("siteip-" . $siteIp);
Expand Down
23 changes: 6 additions & 17 deletions app/Rules/RemoteURL.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace App\Rules;

use App\Network\DNSLookup;
use App\Network\NetworkHelper;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\App;
Expand All @@ -23,23 +23,12 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
}

$host = (string) parse_url($value, PHP_URL_HOST);
$ips = App::make(DNSLookup::class)->getIPs($host);
/** @var NetworkHelper $networkHelper */
$networkHelper = App::make(NetworkHelper::class);

// 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.");
}
// Check IPs
if (!$networkHelper->isValidRemoteHost($host)) {
$fail("Invalid URL: please provide a valid, resolvable Host that does not resolve to local IPs.");
}
}
}
27 changes: 0 additions & 27 deletions tests/Unit/Network/DNSLookupTest.php

This file was deleted.

59 changes: 59 additions & 0 deletions tests/Unit/Network/NetworkHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Tests\Unit\Network;

use App\Network\NetworkHelper;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;

class NetworkHelperTest extends TestCase
{
public function testIpAsHostIsReturned()
{
$object = new NetworkHelper();
$this->assertSame(['127.0.0.1'], $object->getIPs('127.0.0.1'));
}

public function testEmptyArrayIsReturnedForInvalidHost()
{
$object = new NetworkHelper();
$this->assertSame([], $object->getIPs('invalid.host.with.bogus.tld'));
}

public function testIpsAreReturned()
{
$object = new NetworkHelper();
$this->assertGreaterThan(5, $object->getIPs('joomla.org'));
}

#[DataProvider('hostnameDataProvider')]
public function testRemoteIpsAreValidate($ip, $result)
{
$object = new NetworkHelper();
$this->assertEquals($result, $object->isValidRemoteHost($ip));
}

#[DataProvider('ipDataProvider')]
public function testLocalIpsAreForbidden($ip, $result)
{
$object = new NetworkHelper();
$this->assertEquals($result, $object->isValidRemoteIp($ip));
}

public static function hostnameDataProvider(): array
{
return [
['localhost', false],
['joomla.org', true]
];
}

public static function ipDataProvider(): array
{
return [
['127.0.0.1', false],
['10.0.0.1', false],
['8.8.8.8', true]
];
}
}
10 changes: 5 additions & 5 deletions tests/Unit/Rules/RemoteURLTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ public function testRuleHandlesIpsAndHosts($host, $expectedResult, $expectedMess
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.'],
['https://127.0.0.1', false, 'Invalid URL: please provide a valid, resolvable Host that does not resolve to local IPs.'],
['https://localhost', false, 'Invalid URL: please provide a valid, resolvable Host that does not resolve to local IPs.'],
['https://10.0.0.1', false, 'Invalid URL: please provide a valid, resolvable Host that does not resolve to local IPs.'],
['https://invalid.host.tld', false,'Invalid URL: please provide a valid, resolvable Host that does not resolve to local IPs.'],
['https://joomla.org', true, '']
];
}
}
Loading