3 declare(strict_types=1);
5 namespace Laminas\Diactoros\ServerRequestFilter;
7 use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException;
8 use Laminas\Diactoros\Exception\InvalidProxyAddressException;
9 use Psr\Http\Message\ServerRequestInterface;
12 * Modify the URI to reflect the X-Forwarded-* headers.
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
21 final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface
23 public const HEADER_HOST = 'X-FORWARDED-HOST';
24 public const HEADER_PORT = 'X-FORWARDED-PORT';
25 public const HEADER_PROTO = 'X-FORWARDED-PROTO';
27 private const X_FORWARDED_HEADERS = [
34 * @var list<FilterUsingXForwardedHeaders::HEADER_*>
36 private $trustedHeaders;
38 /** @var list<non-empty-string> */
39 private $trustedProxies;
42 * Only allow construction via named constructors
44 * @param list<non-empty-string> $trustedProxies
45 * @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders
47 private function __construct(
48 array $trustedProxies = [],
49 array $trustedHeaders = []
51 $this->trustedProxies = $trustedProxies;
52 $this->trustedHeaders = $trustedHeaders;
55 public function __invoke(ServerRequestInterface $request): ServerRequestInterface
57 $remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? '';
59 if ('' === $remoteAddress || ! is_string($remoteAddress)) {
60 // Should we trigger a warning here?
64 if (! $this->isFromTrustedProxy($remoteAddress)) {
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
78 switch ($headerName) {
79 case self::HEADER_HOST:
80 $uri = $uri->withHost($header);
82 case self::HEADER_PORT:
83 $uri = $uri->withPort((int) $header);
85 case self::HEADER_PROTO:
86 $uri = $uri->withScheme($header);
91 if ($uri !== $originalUri) {
92 return $request->withUri($uri);
99 * Indicate which proxies and which X-Forwarded headers to trust.
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
111 public static function trustProxies(
112 array $proxyCIDRList,
113 array $trustedHeaders = self::X_FORWARDED_HEADERS
115 $proxyCIDRList = self::normalizeProxiesList($proxyCIDRList);
116 self::validateTrustedHeaders($trustedHeaders);
118 return new self($proxyCIDRList, $trustedHeaders);
122 * Trust any X-FORWARDED-* headers from any address.
124 * This is functionally equivalent to calling `trustProxies(['*'])`.
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.).
132 public static function trustAny(): self
134 return self::trustProxies(['*']);
138 * Trust X-Forwarded headers from reserved subnetworks.
140 * This is functionally equivalent to calling `trustProxies()` where the
141 * `$proxcyCIDRList` argument is a list with the following:
147 * - ::1/128 (IPv6 localhost)
148 * - fc00::/7 (IPv6 private networks)
149 * - fe80::/10 (IPv6 local-link addresses)
151 * @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
152 * the list is empty, all X-Forwarded headers are trusted.
153 * @throws InvalidForwardedHeaderNameException
155 public static function trustReservedSubnets(array $trustedHeaders = self::X_FORWARDED_HEADERS): self
157 return self::trustProxies([
162 '::1/128', // ipv6 localhost
163 'fc00::/7', // ipv6 private networks
164 'fe80::/10', // ipv6 local-link addresses
168 private function isFromTrustedProxy(string $remoteAddress): bool
170 foreach ($this->trustedProxies as $proxy) {
171 if (IPRange::matches($remoteAddress, $proxy)) {
179 /** @throws InvalidForwardedHeaderNameException */
180 private static function validateTrustedHeaders(array $headers): void
182 foreach ($headers as $header) {
183 if (! in_array($header, self::X_FORWARDED_HEADERS, true)) {
184 throw InvalidForwardedHeaderNameException::forHeader($header);
190 * @param list<non-empty-string> $proxyCIDRList
191 * @return list<non-empty-string>
192 * @throws InvalidProxyAddressException
194 private static function normalizeProxiesList(array $proxyCIDRList): array
196 $foundWildcard = false;
198 foreach ($proxyCIDRList as $index => $cidr) {
200 unset($proxyCIDRList[$index]);
201 $foundWildcard = true;
205 if (! self::validateProxyCIDR($cidr)) {
206 throw InvalidProxyAddressException::forAddress($cidr);
210 if ($foundWildcard) {
211 $proxyCIDRList[] = '0.0.0.0/0';
212 $proxyCIDRList[] = '::/0';
215 return $proxyCIDRList;
221 private static function validateProxyCIDR($cidr): bool
223 if (! is_string($cidr) || '' === $cidr) {
229 if (false !== strpos($cidr, '/')) {
230 [$address, $mask] = explode('/', $cidr, 2);
234 if (false !== strpos($address, ':')) {
236 return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
247 return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)