-
-
Notifications
You must be signed in to change notification settings - Fork 59
/
FilterUsingXForwardedHeaders.php
257 lines (226 loc) · 7.99 KB
/
FilterUsingXForwardedHeaders.php
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException;
use Laminas\Diactoros\Exception\InvalidProxyAddressException;
use Psr\Http\Message\ServerRequestInterface;
/**
* Modify the URI to reflect the X-Forwarded-* headers.
*
* If the request comes from a trusted proxy, this filter will analyze the
* various X-Forwarded-* headers, if any, and if they are marked as trusted,
* in order to return a new request that composes a URI instance that reflects
* those headers.
*
* @psalm-immutable
*/
final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface
{
public const HEADER_HOST = 'X-FORWARDED-HOST';
public const HEADER_PORT = 'X-FORWARDED-PORT';
public const HEADER_PROTO = 'X-FORWARDED-PROTO';
private const X_FORWARDED_HEADERS = [
self::HEADER_HOST,
self::HEADER_PORT,
self::HEADER_PROTO,
];
/**
* @var list<FilterUsingXForwardedHeaders::HEADER_*>
*/
private $trustedHeaders;
/** @var list<non-empty-string> */
private $trustedProxies;
/**
* Only allow construction via named constructors
*
* @param list<non-empty-string> $trustedProxies
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders
*/
private function __construct(
array $trustedProxies = [],
array $trustedHeaders = []
) {
$this->trustedProxies = $trustedProxies;
$this->trustedHeaders = $trustedHeaders;
}
public function __invoke(ServerRequestInterface $request): ServerRequestInterface
{
$remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? '';
if ('' === $remoteAddress || ! is_string($remoteAddress)) {
// Should we trigger a warning here?
return $request;
}
if (! $this->isFromTrustedProxy($remoteAddress)) {
// Do nothing
return $request;
}
// Update the URI based on the trusted headers
$uri = $originalUri = $request->getUri();
foreach ($this->trustedHeaders as $headerName) {
$header = $request->getHeaderLine($headerName);
if ('' === $header || false !== strpos($header, ',')) {
// Reject empty headers and/or headers with multiple values
continue;
}
switch ($headerName) {
case self::HEADER_HOST:
$uri = $uri->withHost($header);
break;
case self::HEADER_PORT:
$uri = $uri->withPort((int) $header);
break;
case self::HEADER_PROTO:
$scheme = strtolower($header) === 'https' ? 'https' : 'http';
$uri = $uri->withScheme($scheme);
break;
}
}
if ($uri !== $originalUri) {
return $request->withUri($uri);
}
return $request;
}
/**
* Indicate which proxies and which X-Forwarded headers to trust.
*
* @param list<non-empty-string> $proxyCIDRList Each element may
* be an IP address or a subnet specified using CIDR notation; both IPv4
* and IPv6 are supported. The special string "*" will be translated to
* two entries, "0.0.0.0/0" and "::/0". An empty list indicates no
* proxies are trusted.
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidProxyAddressException
* @throws InvalidForwardedHeaderNameException
*/
public static function trustProxies(
array $proxyCIDRList,
array $trustedHeaders = self::X_FORWARDED_HEADERS
): self {
$proxyCIDRList = self::normalizeProxiesList($proxyCIDRList);
self::validateTrustedHeaders($trustedHeaders);
return new self($proxyCIDRList, $trustedHeaders);
}
/**
* Trust any X-FORWARDED-* headers from any address.
*
* This is functionally equivalent to calling `trustProxies(['*'])`.
*
* WARNING: Only do this if you know for certain that your application
* sits behind a trusted proxy that cannot be spoofed. This should only
* be the case if your server is not publicly addressable, and all requests
* are routed via a reverse proxy (e.g., a load balancer, a server such as
* Caddy, when using Traefik, etc.).
*/
public static function trustAny(): self
{
return self::trustProxies(['*']);
}
/**
* Trust X-Forwarded headers from reserved subnetworks.
*
* This is functionally equivalent to calling `trustProxies()` where the
* `$proxcyCIDRList` argument is a list with the following:
*
* - 10.0.0.0/8
* - 127.0.0.0/8
* - 172.16.0.0/12
* - 192.168.0.0/16
* - ::1/128 (IPv6 localhost)
* - fc00::/7 (IPv6 private networks)
* - fe80::/10 (IPv6 local-link addresses)
*
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidForwardedHeaderNameException
*/
public static function trustReservedSubnets(array $trustedHeaders = self::X_FORWARDED_HEADERS): self
{
return self::trustProxies([
'10.0.0.0/8',
'127.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'::1/128', // ipv6 localhost
'fc00::/7', // ipv6 private networks
'fe80::/10', // ipv6 local-link addresses
], $trustedHeaders);
}
private function isFromTrustedProxy(string $remoteAddress): bool
{
foreach ($this->trustedProxies as $proxy) {
if (IPRange::matches($remoteAddress, $proxy)) {
return true;
}
}
return false;
}
/** @throws InvalidForwardedHeaderNameException */
private static function validateTrustedHeaders(array $headers): void
{
foreach ($headers as $header) {
if (! in_array($header, self::X_FORWARDED_HEADERS, true)) {
throw InvalidForwardedHeaderNameException::forHeader($header);
}
}
}
/**
* @param list<non-empty-string> $proxyCIDRList
* @return list<non-empty-string>
* @throws InvalidProxyAddressException
*/
private static function normalizeProxiesList(array $proxyCIDRList): array
{
$foundWildcard = false;
foreach ($proxyCIDRList as $index => $cidr) {
if ($cidr === '*') {
unset($proxyCIDRList[$index]);
$foundWildcard = true;
continue;
}
if (! self::validateProxyCIDR($cidr)) {
throw InvalidProxyAddressException::forAddress($cidr);
}
}
if ($foundWildcard) {
$proxyCIDRList[] = '0.0.0.0/0';
$proxyCIDRList[] = '::/0';
}
return $proxyCIDRList;
}
/**
* @param mixed $cidr
*/
private static function validateProxyCIDR($cidr): bool
{
if (! is_string($cidr) || '' === $cidr) {
return false;
}
$address = $cidr;
$mask = null;
if (false !== strpos($cidr, '/')) {
[$address, $mask] = explode('/', $cidr, 2);
$mask = (int) $mask;
}
if (false !== strpos($address, ':')) {
// is IPV6
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& (
$mask === null
|| (
$mask <= 128
&& $mask >= 0
)
);
}
// is IPV4
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
&& (
$mask === null
|| (
$mask <= 32
&& $mask >= 0
)
);
}
}