-
-
Notifications
You must be signed in to change notification settings - Fork 628
Expand file tree
/
Copy pathRemoteUrlValidator.php
More file actions
110 lines (83 loc) · 3.09 KB
/
RemoteUrlValidator.php
File metadata and controls
110 lines (83 loc) · 3.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?php
namespace Statamic\Imaging;
use Statamic\Exceptions\InvalidRemoteUrlException;
use Statamic\Support\Str;
class RemoteUrlValidator
{
protected $resolver;
public function __construct(?callable $resolver = null)
{
$this->resolver = $resolver ?? fn ($host) => dns_get_record($host, DNS_A + DNS_AAAA) ?: [];
}
public function parse($url)
{
$parsed = parse_url($url);
if (! is_array($parsed)) {
throw new InvalidRemoteUrlException('Invalid URL.');
}
$scheme = strtolower($parsed['scheme'] ?? '');
if (! in_array($scheme, ['http', 'https'])) {
throw new InvalidRemoteUrlException('Only http and https URLs are allowed.');
}
if (isset($parsed['user']) || isset($parsed['pass'])) {
throw new InvalidRemoteUrlException('URLs with credentials are not allowed.');
}
$host = $parsed['host'] ?? null;
if (! is_string($host) || $host === '') {
throw new InvalidRemoteUrlException('URL host is required.');
}
$host = Str::lower(trim($host));
if ($host !== trim($host, '.')) {
throw new InvalidRemoteUrlException('Invalid URL host.');
}
if (! $this->isValidHost($host)) {
throw new InvalidRemoteUrlException('Invalid URL host.');
}
$this->ensureHostResolvesToPublicIps($host);
$port = isset($parsed['port']) ? ':'.$parsed['port'] : '';
return [
'path' => Str::after($parsed['path'] ?? '/', '/'),
'base' => $scheme.'://'.$host.$port,
'query' => $parsed['query'] ?? null,
];
}
public function validate($url)
{
$this->parse($url);
}
protected function isValidHost($host)
{
return filter_var($host, FILTER_VALIDATE_IP)
|| filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
}
protected function ensureHostResolvesToPublicIps($host)
{
if (filter_var($host, FILTER_VALIDATE_IP)) {
$this->assertPublicIp($host);
return;
}
$records = call_user_func($this->resolver, $host);
$ips = collect($records)->flatMap(function ($record) {
return [$record['ip'] ?? null, $record['ipv6'] ?? null];
})->filter()->values()->all();
if (empty($ips)) {
throw new InvalidRemoteUrlException('Unable to resolve URL host.');
}
foreach ($ips as $ip) {
$this->assertPublicIp($ip);
}
}
protected function assertPublicIp($ip)
{
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$packed = inet_pton($ip);
if ($packed !== false && substr($packed, 0, 12) === "\0\0\0\0\0\0\0\0\0\0\xff\xff") {
$ip = inet_ntop(substr($packed, 12));
}
}
$result = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if (! $result) {
throw new InvalidRemoteUrlException('Destination IP is not publicly routable.');
}
}
}