fcac3753ea0e01b3368f0212ff31cd79e259d242
[GitHub/WoltLab/WCF.git] /
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Laminas\Diactoros\ServerRequestFilter;
6
7 use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException;
8 use Laminas\Diactoros\Exception\InvalidProxyAddressException;
9 use Psr\Http\Message\ServerRequestInterface;
10
11 /**
12 * Modify the URI to reflect the X-Forwarded-* headers.
13 *
14 * If the request comes from a trusted proxy, this filter will analyze the
15 * various X-Forwarded-* headers, if any, and if they are marked as trusted,
16 * in order to return a new request that composes a URI instance that reflects
17 * those headers.
18 *
19 * @psalm-immutable
20 */
21 final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface
22 {
23 public const HEADER_HOST = 'X-FORWARDED-HOST';
24 public const HEADER_PORT = 'X-FORWARDED-PORT';
25 public const HEADER_PROTO = 'X-FORWARDED-PROTO';
26
27 private const X_FORWARDED_HEADERS = [
28 self::HEADER_HOST,
29 self::HEADER_PORT,
30 self::HEADER_PROTO,
31 ];
32
33 /**
34 * @var list<FilterUsingXForwardedHeaders::HEADER_*>
35 */
36 private $trustedHeaders;
37
38 /** @var list<non-empty-string> */
39 private $trustedProxies;
40
41 /**
42 * Only allow construction via named constructors
43 *
44 * @param list<non-empty-string> $trustedProxies
45 * @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders
46 */
47 private function __construct(
48 array $trustedProxies = [],
49 array $trustedHeaders = []
50 ) {
51 $this->trustedProxies = $trustedProxies;
52 $this->trustedHeaders = $trustedHeaders;
53 }
54
55 public function __invoke(ServerRequestInterface $request): ServerRequestInterface
56 {
57 $remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? '';
58
59 if ('' === $remoteAddress || ! is_string($remoteAddress)) {
60 // Should we trigger a warning here?
61 return $request;
62 }
63
64 if (! $this->isFromTrustedProxy($remoteAddress)) {
65 // Do nothing
66 return $request;
67 }
68
69 // Update the URI based on the trusted headers
70 $uri = $originalUri = $request->getUri();
71 foreach ($this->trustedHeaders as $headerName) {
72 $header = $request->getHeaderLine($headerName);
73 if ('' === $header || false !== strpos($header, ',')) {
74 // Reject empty headers and/or headers with multiple values
75 continue;
76 }
77
78 switch ($headerName) {
79 case self::HEADER_HOST:
80 $uri = $uri->withHost($header);
81 break;
82 case self::HEADER_PORT:
83 $uri = $uri->withPort((int) $header);
84 break;
85 case self::HEADER_PROTO:
86 $uri = $uri->withScheme($header);
87 break;
88 }
89 }
90
91 if ($uri !== $originalUri) {
92 return $request->withUri($uri);
93 }
94
95 return $request;
96 }
97
98 /**
99 * Indicate which proxies and which X-Forwarded headers to trust.
100 *
101 * @param list<non-empty-string> $proxyCIDRList Each element may
102 * be an IP address or a subnet specified using CIDR notation; both IPv4
103 * and IPv6 are supported. The special string "*" will be translated to
104 * two entries, "0.0.0.0/0" and "::/0". An empty list indicates no
105 * proxies are trusted.
106 * @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
107 * the list is empty, all X-Forwarded headers are trusted.
108 * @throws InvalidProxyAddressException
109 * @throws InvalidForwardedHeaderNameException
110 */
111 public static function trustProxies(
112 array $proxyCIDRList,
113 array $trustedHeaders = self::X_FORWARDED_HEADERS
114 ): self {
115 $proxyCIDRList = self::normalizeProxiesList($proxyCIDRList);
116 self::validateTrustedHeaders($trustedHeaders);
117
118 return new self($proxyCIDRList, $trustedHeaders);
119 }
120
121 /**
122 * Trust any X-FORWARDED-* headers from any address.
123 *
124 * This is functionally equivalent to calling `trustProxies(['*'])`.
125 *
126 * WARNING: Only do this if you know for certain that your application
127 * sits behind a trusted proxy that cannot be spoofed. This should only
128 * be the case if your server is not publicly addressable, and all requests
129 * are routed via a reverse proxy (e.g., a load balancer, a server such as
130 * Caddy, when using Traefik, etc.).
131 */
132 public static function trustAny(): self
133 {
134 return self::trustProxies(['*']);
135 }
136
137 /**
138 * Trust X-Forwarded headers from reserved subnetworks.
139 *
140 * This is functionally equivalent to calling `trustProxies()` where the
141 * `$proxcyCIDRList` argument is a list with the following:
142 *
143 * - 10.0.0.0/8
144 * - 127.0.0.0/8
145 * - 172.16.0.0/12
146 * - 192.168.0.0/16
147 * - ::1/128 (IPv6 localhost)
148 * - fc00::/7 (IPv6 private networks)
149 * - fe80::/10 (IPv6 local-link addresses)
150 *
151 * @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
152 * the list is empty, all X-Forwarded headers are trusted.
153 * @throws InvalidForwardedHeaderNameException
154 */
155 public static function trustReservedSubnets(array $trustedHeaders = self::X_FORWARDED_HEADERS): self
156 {
157 return self::trustProxies([
158 '10.0.0.0/8',
159 '127.0.0.0/8',
160 '172.16.0.0/12',
161 '192.168.0.0/16',
162 '::1/128', // ipv6 localhost
163 'fc00::/7', // ipv6 private networks
164 'fe80::/10', // ipv6 local-link addresses
165 ], $trustedHeaders);
166 }
167
168 private function isFromTrustedProxy(string $remoteAddress): bool
169 {
170 foreach ($this->trustedProxies as $proxy) {
171 if (IPRange::matches($remoteAddress, $proxy)) {
172 return true;
173 }
174 }
175
176 return false;
177 }
178
179 /** @throws InvalidForwardedHeaderNameException */
180 private static function validateTrustedHeaders(array $headers): void
181 {
182 foreach ($headers as $header) {
183 if (! in_array($header, self::X_FORWARDED_HEADERS, true)) {
184 throw InvalidForwardedHeaderNameException::forHeader($header);
185 }
186 }
187 }
188
189 /**
190 * @param list<non-empty-string> $proxyCIDRList
191 * @return list<non-empty-string>
192 * @throws InvalidProxyAddressException
193 */
194 private static function normalizeProxiesList(array $proxyCIDRList): array
195 {
196 $foundWildcard = false;
197
198 foreach ($proxyCIDRList as $index => $cidr) {
199 if ($cidr === '*') {
200 unset($proxyCIDRList[$index]);
201 $foundWildcard = true;
202 continue;
203 }
204
205 if (! self::validateProxyCIDR($cidr)) {
206 throw InvalidProxyAddressException::forAddress($cidr);
207 }
208 }
209
210 if ($foundWildcard) {
211 $proxyCIDRList[] = '0.0.0.0/0';
212 $proxyCIDRList[] = '::/0';
213 }
214
215 return $proxyCIDRList;
216 }
217
218 /**
219 * @param mixed $cidr
220 */
221 private static function validateProxyCIDR($cidr): bool
222 {
223 if (! is_string($cidr) || '' === $cidr) {
224 return false;
225 }
226
227 $address = $cidr;
228 $mask = null;
229 if (false !== strpos($cidr, '/')) {
230 [$address, $mask] = explode('/', $cidr, 2);
231 $mask = (int) $mask;
232 }
233
234 if (false !== strpos($address, ':')) {
235 // is IPV6
236 return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
237 && (
238 $mask === null
239 || (
240 $mask <= 128
241 && $mask >= 0
242 )
243 );
244 }
245
246 // is IPV4
247 return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
248 && (
249 $mask === null
250 || (
251 $mask <= 32
252 && $mask >= 0
253 )
254 );
255 }
256 }