From e08e52efcf470e3d048cd58ebf4864a40757ed83 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 11:23:22 -0500 Subject: [PATCH 01/45] security: add request filter functionality for handling x-forwarded-* headers We have always accepted `X-Forwarded-*` headers as valid when generating a URI instance via `marshalUriFromSapi()` (and, by extension, `ServerRequestFactory::fromGlobals()`). However, these headers should only be used if there is a trusted proxy in front of the application. On the flip side, changing the behavior now would result in breakage of existing applications built on this library. This patch does the following. First, it creates a new function `Laminas\Diactoros\marshalUriFromSapiSafely()`. This function ignores the `X-Forwarded-*` headers when generating the URI. `ServerRequestFactory::fromGlobals()` now uses this new function internally, and `Laminas\Diactoros\marshalUriFromSapi()` has been marked deprecated. Next, we have added `Laminas\Diactoros\RequestFilter\RequestFilterInterface`, which defines the single method `filterRequest(ServerRequestInterface $request): ServerRequestInterface`. `ServerRequestFactory::fromGlobals()` now optionally accepts an implementation, and passes the request instance it generates to it before returning a request. If no instance is provided, for version 2, it will create an instance of a new implementation, `LegacyXForwardedHeaderFilter` marked to trust any proxy; for version 3, it will create and use `NoOpRequestFilter`. `LegacyXForwardedHeaderFilter` defines additional methods for the purpose of configuring what proxies to trust: - `trustAny()` indicates all proxies (and all `X-Forwarded-*` headers) should be trusted. - `trustProxies(string|string[] $proxyAddresses, string[] $trustedHeaders = LegacyXForwardedHeaderFilter::X_FORWARDED_HEADERS)` allows indicating one or more proxies that are trusted, as well as which `X-Forwarded-*` headers to trust. Proxy addresses may be specified as absolute IP addresses, or as CIDR subnet masks (e.g., 192.168.0.0/16, 10.0.0.0/24, etc.). When invoked, if the proxy is trusted, the filter loops through the trusted headers and sees if any are present in the request; if so, it will update the URI with matched values, and return a new request instance with the new URI instance. This approach allows us to expand functionality to address [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239) at a later date. **The decision to make this opt-in versus secure-by-default for version 2 is to prevent a BC break for end-users who relied on the fact that we used these `X-Forwarded-*` headers when constructing the URI.** For version 3, we will have secure-by-default settings. We will need to add functionality to the [mezzio/mezzio](https://github.com/mezzio/mezzio) package to support using a `RequestFilterInterface` instance with the server request factory within the current v2 series, and could pontentially update the mezzio skeleton immediately to use the `NoOpRequestFilter` by default. Signed-off-by: Matthew Weier O'Phinney --- composer.json | 5 +- .../InvalidForwardedHeaderNameException.php | 23 ++ .../InvalidProxyAddressException.php | 29 +++ src/RequestFilter/IPRange.php | 102 +++++++++ .../LegacyXForwardedHeaderFilter.php | 204 +++++++++++++++++ src/RequestFilter/NoOpRequestFilter.php | 15 ++ src/RequestFilter/RequestFilterInterface.php | 12 + src/ServerRequestFactory.php | 23 +- src/functions/marshal_uri_from_sapi.php | 5 + .../marshal_uri_from_sapi_safely.php | 186 +++++++++++++++ test/RequestFilter/IPRangeTest.php | 102 +++++++++ .../LegacyXForwardedHeaderFilterTest.php | 213 ++++++++++++++++++ test/RequestFilter/NoOpRequestFilterTest.php | 20 ++ test/ServerRequestFactoryTest.php | 31 +++ 14 files changed, 964 insertions(+), 6 deletions(-) create mode 100644 src/Exception/InvalidForwardedHeaderNameException.php create mode 100644 src/Exception/InvalidProxyAddressException.php create mode 100644 src/RequestFilter/IPRange.php create mode 100644 src/RequestFilter/LegacyXForwardedHeaderFilter.php create mode 100644 src/RequestFilter/NoOpRequestFilter.php create mode 100644 src/RequestFilter/RequestFilterInterface.php create mode 100644 src/functions/marshal_uri_from_sapi_safely.php create mode 100644 test/RequestFilter/IPRangeTest.php create mode 100644 test/RequestFilter/LegacyXForwardedHeaderFilterTest.php create mode 100644 test/RequestFilter/NoOpRequestFilterTest.php diff --git a/composer.json b/composer.json index 27dffe53..2ebdfa75 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "config": { "sort-packages": true, "platform": { - "php": "7.3.99" + "php": "7.4.99" } }, "extra": { @@ -31,7 +31,7 @@ } }, "require": { - "php": "^7.3 || ~8.0.0 || ~8.1.0", + "php": "^7.4 || ~8.0.0 || ~8.1.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.0" }, @@ -63,6 +63,7 @@ "src/functions/marshal_method_from_sapi.php", "src/functions/marshal_protocol_version_from_sapi.php", "src/functions/marshal_uri_from_sapi.php", + "src/functions/marshal_uri_from_sapi_safely.php", "src/functions/normalize_server.php", "src/functions/normalize_uploaded_files.php", "src/functions/parse_cookie_header.php", diff --git a/src/Exception/InvalidForwardedHeaderNameException.php b/src/Exception/InvalidForwardedHeaderNameException.php new file mode 100644 index 00000000..f2ee3aca --- /dev/null +++ b/src/Exception/InvalidForwardedHeaderNameException.php @@ -0,0 +1,23 @@ + 32) { + return false; + } + + $ip = ip2long($ip); + $subnet = ip2long($subnet); + if (false === $ip || false === $subnet) { + // Invalid data + return false; + } + + return 0 === substr_compare( + sprintf("%032b", $ip), + sprintf("%032b", $subnet), + 0, + $mask + ); + } + + public static function matchesIPv6(string $ip, string $cidr): bool + { + $mask = 128; + $subnet = $cidr; + + if (false !== strpos($cidr, '/')) { + [$subnet, $mask] = explode('/', $cidr, 2); + $mask = (int) $mask; + } + + if ($mask < 0 || $mask > 128) { + return false; + } + + $ip = inet_pton($ip); + $subnet = inet_pton($subnet); + + if (false == $ip || false == $subnet) { + // Invalid data + return false; + } + + // mask 0: if it's a valid IP, it's valid + if ($mask === 0) { + return (bool) unpack('n*', $ip); + } + + // @see http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet, MW answer + $binMask = str_repeat("f", intval($mask / 4)); + switch ($mask % 4) { + case 0: + break; + case 1: + $binMask .= "8"; + break; + case 2: + $binMask .= "c"; + break; + case 3: + $binMask .= "e"; + break; + } + + $binMask = str_pad($binMask, 32, '0'); + $binMask = pack("H*", $binMask); + + return ($ip & $binMask) === $subnet; + } +} diff --git a/src/RequestFilter/LegacyXForwardedHeaderFilter.php b/src/RequestFilter/LegacyXForwardedHeaderFilter.php new file mode 100644 index 00000000..a3aedbf9 --- /dev/null +++ b/src/RequestFilter/LegacyXForwardedHeaderFilter.php @@ -0,0 +1,204 @@ +getServerParams()['REMOTE_ADDR'] ?? ''; + + if ('' === $remoteAddress) { + // Should we trigger a warning here? + return $request; + } + + if (! $this->trustAny && ! $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) { + continue; + } + + switch ($headerName) { + case self::HEADER_HOST: + $uri = $uri->withHost($header); + break; + case self::HEADER_PORT: + $uri = $uri->withPort($header); + break; + case self::HEADER_PROTO: + $uri = $uri->withScheme($header); + break; + default: + break; + } + } + + if ($uri !== $originalUri) { + return $request->withUri($uri); + } + + return $request; + } + + /** + * Trust X-FORWARDED-* headers from any address. + * + * 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 function trustAny(): void + { + $this->trustAny = true; + $this->trustedHeaders = self::X_FORWARDED_HEADERS; + } + + /** + * @param string|string[] $proxies + * @param array $trustedHeaders + * @throws InvalidProxyAddressException + * @throws InvalidForwardedHeaderNameException + */ + public function trustProxies( + $proxies, + array $trustedHeaders = self::X_FORWARDED_HEADERS + ): void { + $proxies = $this->normalizeProxiesList($proxies); + $this->validateTrustedHeaders($trustedHeaders); + + $this->trustAny = false; + $this->trustedProxies = $proxies; + $this->trustedHeaders = $trustedHeaders; + } + + public function isFromTrustedProxy(string $remoteAddress): bool + { + if ($this->trustAny) { + return true; + } + + foreach ($this->trustedProxies as $proxy) { + if (IPRange::matches($remoteAddress, $proxy)) { + return true; + } + } + + return false; + } + + /** @throws InvalidForwardedHeaderNameException */ + private function validateTrustedHeaders(array $headers): void + { + foreach ($headers as $header) { + if (! in_array($header, self::X_FORWARDED_HEADERS, true)) { + throw InvalidForwardedHeaderNameException::forHeader($header); + } + } + } + + private function normalizeProxiesList($proxies): array + { + if (! is_array($proxies) && ! is_string($proxies)) { + throw InvalidProxyAddressException::forInvalidProxyArgument($proxies); + } + + $proxies = is_array($proxies) ? $proxies : [$proxies]; + + foreach ($proxies as $proxy) { + if (! $this->validateProxyCIDR($proxy)) { + throw InvalidProxyAddressException::forAddress($proxy); + } + } + + return $proxies; + } + + /** + * @param mixed $cidr + * @throws InvalidCIDRException + */ + private function validateProxyCIDR($cidr): bool + { + if (! is_string($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 + ) + ); + } + + private function getHeaderListToRemove(): array + { + $toRemove = self::X_FORWARDED_HEADERS; + foreach ($this->trustedHeaders as $header) { + $index = array_search($header, $toRemove, true); + if (! is_int($index)) { + continue; + } + + unset($toRemove[$index]); + } + + return $toRemove; + } +} diff --git a/src/RequestFilter/NoOpRequestFilter.php b/src/RequestFilter/NoOpRequestFilter.php new file mode 100644 index 00000000..0a162b97 --- /dev/null +++ b/src/RequestFilter/NoOpRequestFilter.php @@ -0,0 +1,15 @@ +trustAny(); + } + $server = normalizeServer( $server ?: $_SERVER, is_callable(self::$apacheRequestHeaders) ? self::$apacheRequestHeaders : null @@ -62,10 +77,10 @@ public static function fromGlobals( $cookies = parseCookieHeader($headers['cookie']); } - return new ServerRequest( + return $requestFilter->filterRequest(new ServerRequest( $server, $files, - marshalUriFromSapi($server, $headers), + marshalUriFromSapiSafely($server, $headers), marshalMethodFromSapi($server), 'php://input', $headers, @@ -73,7 +88,7 @@ public static function fromGlobals( $query ?: $_GET, $body ?: $_POST, marshalProtocolVersionFromSapi($server) - ); + )); } /** diff --git a/src/functions/marshal_uri_from_sapi.php b/src/functions/marshal_uri_from_sapi.php index 778d31bf..0268ce1f 100644 --- a/src/functions/marshal_uri_from_sapi.php +++ b/src/functions/marshal_uri_from_sapi.php @@ -22,6 +22,11 @@ * * @param array $server SAPI parameters * @param array $headers HTTP request headers + * @deprecated This function is deprecated as of 2.11.1, and will be removed in + * 3.0.0. For security purposes, we recommend using Laminas\Diactoros\marshalUriFromSapiSafely, + * and a Laminas\Diactoros\RequestFilter\RequestFilterInterface + * implmentation if you need the ability to use X-Forwarded-* headers to + * modify the generated URI. */ function marshalUriFromSapi(array $server, array $headers) : Uri { diff --git a/src/functions/marshal_uri_from_sapi_safely.php b/src/functions/marshal_uri_from_sapi_safely.php new file mode 100644 index 00000000..c80be9e8 --- /dev/null +++ b/src/functions/marshal_uri_from_sapi_safely.php @@ -0,0 +1,186 @@ +withScheme($scheme); + + // Set the host + [$host, $port] = $marshalHostAndPort($server); + if (! empty($host)) { + $uri = $uri->withHost($host); + if (! empty($port)) { + $uri = $uri->withPort($port); + } + } + + // URI path + $path = $marshalRequestPath($server); + + // Strip query string + $path = explode('?', $path, 2)[0]; + + // URI query + $query = ''; + if (isset($server['QUERY_STRING'])) { + $query = ltrim($server['QUERY_STRING'], '?'); + } + + // URI fragment + $fragment = ''; + if (strpos($path, '#') !== false) { + [$path, $fragment] = explode('#', $path, 2); + } + + return $uri + ->withPath($path) + ->withFragment($fragment) + ->withQuery($query); +} diff --git a/test/RequestFilter/IPRangeTest.php b/test/RequestFilter/IPRangeTest.php new file mode 100644 index 00000000..3cbcfe22 --- /dev/null +++ b/test/RequestFilter/IPRangeTest.php @@ -0,0 +1,102 @@ + + */ + public function IPv4Data(): array + { + return [ + 'valid - exact (no mask; /32 equiv)' => [true, '192.168.1.1', '192.168.1.1'], + 'valid - entirety of class-c (/1)' => [true, '192.168.1.1', '192.168.1.1/1'], + 'valid - class-c private subnet (/24)' => [true, '192.168.1.1', '192.168.1.0/24'], + 'valid - any subnet (/0)' => [true, '1.2.3.4', '0.0.0.0/0'], + 'valid - subnet expands to all' => [true, '1.2.3.4', '192.168.1.0/0'], + 'invalid - class-a invalid subnet' => [false, '192.168.1.1', '1.2.3.4/1'], + 'invalid - CIDR mask out-of-range' => [false, '192.168.1.1', '192.168.1.1/33'], + 'invalid - invalid cidr notation' => [false, '1.2.3.4', '256.256.256/0'], + 'invalid - invalid IP address' => [false, 'an_invalid_ip', '192.168.1.0/24'], + 'invalid - empty IP address' => [false, '', '1.2.3.4/1'], + 'invalid - proxy wildcard' => [false, '192.168.20.13', '*'], + 'invalid - proxy missing netmask' => [false, '192.168.20.13', '0.0.0.0'], + 'invalid - request IP with invalid proxy wildcard' => [false, '0.0.0.0', '*'], + ]; + } + + /** + * @dataProvider IPv4Data + */ + public function testIPv4(bool $result, string $remoteAddr, string $cidr): void + { + $this->assertSame($result, IPRange::matchesIPv4($remoteAddr, $cidr)); + } + + /** + * @psalm-return array + */ + public function IPv6Data(): array + { + return [ + 'valid - ipv4 subnet' => [true, '2a01:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'], + 'valid - exact' => [true, '0:0:0:0:0:0:0:1', '::1'], + 'valid - all subnets' => [true, '0:0:603:0:396e:4789:8e99:0001', '::/0'], + 'valid - subnet expands to all' => [true, '0:0:603:0:396e:4789:8e99:0001', '2a01:198:603:0::/0'], + 'invalid - not in subnet' => [false, '2a00:198:603:0:396e:4789:8e99:890f', '2a01:198:603:0::/65'], + 'invalid - does not match exact' => [false, '2a01:198:603:0:396e:4789:8e99:890f', '::1'], + 'invalid - compressed notation, does not match exact' => [false, '0:0:603:0:396e:4789:8e99:0001', '::1'], + 'invalid - garbage IP' => [false, '}__test|O:21:"JDatabaseDriverMysqli":3:{s:2', '::1'], + 'invalid - invalid cidr' => [false, '2a01:198:603:0:396e:4789:8e99:890f', 'unknown'], + 'invalid - empty IP address' => [false, '', '::1'], + ]; + } + + /** + * @dataProvider IPv6Data + */ + public function testIPv6(bool $result, string $remoteAddr, string $cidr): void + { + $this->assertSame($result, IPRange::matchesIPv6($remoteAddr, $cidr)); + } + + /** + * @psalm-return iterable + */ + public function combinedData(): iterable + { + foreach ($this->IPv4Data() as $test => $data) { + $name = "IPv4 - {$test}"; + yield $name => $data; + } + + foreach ($this->IPv6Data() as $test => $data) { + $name = "IPv6 - {$test}"; + yield $name => $data; + } + } + + /** @dataProvider combinedData */ + public function testCombinedIPv4AndIPv6Pool(bool $result, string $remoteAddr, string $cidr): void + { + $this->assertSame($result, IPRange::matches($remoteAddr, $cidr)); + } +} diff --git a/test/RequestFilter/LegacyXForwardedHeaderFilterTest.php b/test/RequestFilter/LegacyXForwardedHeaderFilterTest.php new file mode 100644 index 00000000..7218561d --- /dev/null +++ b/test/RequestFilter/LegacyXForwardedHeaderFilterTest.php @@ -0,0 +1,213 @@ + '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustProxies('192.168.1.0/24'); + + $filteredRequest = $filter->filterRequest($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(4433, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThoseHeadersForTrustedProxy(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustProxies( + '192.168.1.0/24', + [$filter::HEADER_HOST, $filter::HEADER_PROTO] + ); + + $filteredRequest = $filter->filterRequest($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(80, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '10.0.0.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustProxies('192.168.1.0/24'); + + $filteredRequest = $filter->filterRequest($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertSame($request->getUri(), $filteredUri); + } + + /** @psalm-return iterable */ + public function trustedProxyList(): iterable + { + yield 'private-class-a-subnet' => ['10.1.1.1']; + yield 'private-class-c-subnet' => ['192.168.1.1']; + } + + /** @dataProvider trustedProxyList */ + public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwardedHeadersForTrustedProxies( + string $remoteAddr + ): void { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + + $filteredRequest = $filter->filterRequest($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(4433, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + /** @dataProvider trustedProxyList */ + public function testTrustingProxyListWithSpecificTrustedHeadersTrustsOnlyThoseHeaders(string $remoteAddr): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustProxies( + ['192.168.1.0/24', '10.1.0.0/16'], + [$filter::HEADER_HOST, $filter::HEADER_PROTO] + ); + + $filteredRequest = $filter->filterRequest($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(80, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + /** @psalm-return iterable */ + public function untrustedProxyList(): iterable + { + yield 'private-class-a-subnet' => ['10.0.0.1']; + yield 'private-class-c-subnet' => ['192.168.168.1']; + } + + /** @dataProvider untrustedProxyList */ + public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $remoteAddr): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + + $this->assertSame($request, $filter->filterRequest($request)); + } + + public function testPassingInvalidStringAddressForProxyRaisesException(): void + { + $filter = new LegacyXForwardedHeaderFilter(); + $this->expectException(InvalidProxyAddressException::class); + $filter->trustProxies('192.168.1'); + } + + public function testPassingInvalidAddressInProxyListRaisesException(): void + { + $filter = new LegacyXForwardedHeaderFilter(); + $this->expectException(InvalidProxyAddressException::class); + $filter->trustProxies(['192.168.1']); + } + + public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void + { + $filter = new LegacyXForwardedHeaderFilter(); + $this->expectException(InvalidForwardedHeaderNameException::class); + $filter->trustProxies('192.168.1.0/24', ['Host']); + } +} diff --git a/test/RequestFilter/NoOpRequestFilterTest.php b/test/RequestFilter/NoOpRequestFilterTest.php new file mode 100644 index 00000000..2a3c19eb --- /dev/null +++ b/test/RequestFilter/NoOpRequestFilterTest.php @@ -0,0 +1,20 @@ +assertSame($request, $filter->filterRequest($request)); + } +} diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index 085908c2..74a37cd0 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -4,11 +4,13 @@ namespace LaminasTest\Diactoros; +use Laminas\Diactoros\RequestFilter\RequestFilterInterface; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; use Laminas\Diactoros\UploadedFile; use Laminas\Diactoros\Uri; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ServerRequestInterface; use ReflectionMethod; use ReflectionProperty; use UnexpectedValueException; @@ -722,4 +724,33 @@ public function testDoesNotMarshalAllContentPrefixedServerVarsAsHeaders( $this->assertSame($expectedHeaderValue, $request->getHeaderLine($headerName)); $this->assertSame($expectedServerValue, $request->getServerParams()[$key]); } + + public function testReturnsFilteredRequestBasedOnRequestFilterProvided(): void + { + $expectedRequest = new ServerRequest(); + $filter = new class($expectedRequest) implements RequestFilterInterface { + private ServerRequestInterface $request; + + public function __construct(ServerRequestInterface $request) + { + $this->request = $request; + } + + public function filterRequest(ServerRequestInterface $request): ServerRequestInterface + { + return $this->request; + } + }; + + $request = ServerRequestFactory::fromGlobals( + ['REMOTE_ADDR' => '127.0.0.1'], + ['foo' => 'bar'], + null, + null, + null, + $filter + ); + + $this->assertSame($expectedRequest, $request); + } } From d8798a24dd1057a46441c437ae131bad9419e9ad Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 13:05:33 -0500 Subject: [PATCH 02/45] docs: document changes introduced by RequestFilter patch Adds the following documentation: - A "Request Filters" chapter, detailing the new interface and its implementations. - Updates to the "API" chapter, detailing: - The change to the `ServerRequestFactory::fromGlobals()` signature. - The change in behavior in `ServerRequestFactory::fromGlobals()`, as well as changes planned for version 3. - The addition of `marshalUriFromSapiSafely()` function. - The deprecation of the `marshalUriFromSapi()` function. - Adds a "Forward Migration to Version 3" chapter, detailing how users should update their code to make use of request filters. Code changes were made, due to discoveries during documentation: - `marshalUriFromSapiSafely()` was still taking the `X-Forwarded-Proto` header into account; it no longer does. - Removed unneeded whitespace from constant declarations in the `LegacyXForwardedHeaderFilter` class file. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/api.md | 33 +++++-- docs/book/v2/forward-migration.md | 22 +++++ docs/book/v2/request-filters.md | 99 +++++++++++++++++++ mkdocs.yml | 2 + .../LegacyXForwardedHeaderFilter.php | 6 +- .../marshal_uri_from_sapi_safely.php | 8 +- 6 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 docs/book/v2/forward-migration.md create mode 100644 docs/book/v2/request-filters.md diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index c608702e..1ed558d4 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -104,10 +104,10 @@ $jsonResponse = new JsonResponse($data, 422, [ ## ServerRequestFactory -This static class can be used to marshal a `ServerRequest` instance from the PHP environment. The -primary entry point is `Laminas\Diactoros\ServerRequestFactory::fromGlobals(array $server, array -$query, array $body, array $cookies, array $files)`. This method will create a new `ServerRequest` -instance with the data provided. Examples of usage are: +This static class can be used to marshal a `ServerRequest` instance from the PHP environment. +The primary entry point is `Laminas\Diactoros\ServerRequestFactory::fromGlobals(array $server, array $query, array $body, array $cookies, array $files, ?Laminas\Diactoros\RequestFilter\RequestFilterInterface $requestFilter)`. +This method will create a new `ServerRequest` instance with the data provided. +Examples of usage are: ```php // Returns new ServerRequest instance, using values from superglobals: @@ -124,8 +124,23 @@ $request = ServerRequestFactory::fromGlobals( $_COOKIE, $_FILES ); + +### Request Filter + +Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. +This should be a `null` value, or an instance of [`Laminas\Diactoros\RequestFilter\RequestFilterInterface`](request-filters.md). +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter`](request-filters.md#legacyxforwardedheaderfilter) instance configured as follows: + +```php +$requestFilter = new LegacyXForwardedHeaderFilter(); +$requestFilter->trustAny(); ``` +The request filter is called on the generated server request instance, and its result is returned from `fromGlobals()`. + +**For version 3 releases, this method will switch to using a `Laminas\Diactoros\RequestFilter\NoOpRequestFilter` by default.** +If you are using this factory method directly, please be aware and update your code accordingly. + ### ServerRequestFactory Helper Functions In order to create the various artifacts required by a `ServerRequest` instance, @@ -137,8 +152,14 @@ and even the `Cookie` header. These include: (its main purpose is to aggregate the `Authorization` header in the SAPI params when under Apache) - `Laminas\Diactoros\marshalProtocolVersionFromSapi(array $server) : string` -- `Laminas\Diactoros\marshalMethodFromSapi(array $server) : string` -- `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers) : Uri` +- `Laminas\Diactoros\marshalMethodFromSapi(array $server) : string`. +- `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers) : Uri`. + Please note: **this function is deprecated as of version 2.11.1**. + Use `Laminas\Diactoros\marshalUriFromSapiSafely()` instead. + This function is no longer used in `ServerRequestFactory::fromGlobals()`. +- `Laminas\Diactoros\marshalUriFromSapiSafely(array $server, array $headers) : Uri`. + This function differs from `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers)` in that it never considers `X-Forwarded-*` headers when generating the `Uri` instance composed in the generated `ServerRequest`. + It is the implementation used since version 2.11.1. - `Laminas\Diactoros\marshalHeadersFromSapi(array $server) : array` - `Laminas\Diactoros\parseCookieHeader(string $header) : array` - `Laminas\Diactoros\createUploadedFile(array $spec) : UploadedFile` (creates the diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md new file mode 100644 index 00000000..770c9405 --- /dev/null +++ b/docs/book/v2/forward-migration.md @@ -0,0 +1,22 @@ +# Preparing for Version 3 + +## RequestFilterInterface defaults + +Introduced in version 2.11.1, the `Laminas\Diactoros\RequestFilter\RequestFilterInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. +The primary use case is to allow modifying the generated URI based on the presence of headers such as `X-Forwarded-Host`. +When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. +(We have also traditionally examined the `X-Forwarded-Proto` header; some implementations examine the `X-Forwarded-Port` header as well.) + +To accommodate this use case, we created `Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter`. +(The "Legacy" verbiage is because a [new RFC (7239)](https://datatracker.ietf.org/doc/html/rfc7239) provides an official specification for this behavior via a new `Forwarded` header.) + +Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. +(This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) +`LegacyXForwardedHeaderFilter` provides methods to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. +To prevent backwards compatibility breaks, we use this filter by default, marked to trust any proxy. +However, **in version 3, we will use a no-op filter by default**. + +Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\RequestFilter\RequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `LegacyXForwardedHeaderFilter` if you depend on this functionality. +If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\RequestFilter\NoOpRequestFilter` as the configured `RequestFilterInterface` in your application immediately. + +We will update this documentation with a link to the related functionality in mezzio/mezzio when it is published. diff --git a/docs/book/v2/request-filters.md b/docs/book/v2/request-filters.md new file mode 100644 index 00000000..79804e55 --- /dev/null +++ b/docs/book/v2/request-filters.md @@ -0,0 +1,99 @@ +# Request Filters + +> - Since laminas/laminas-diactoros 2.11.1 + +Request filters allow you to modify the initial state of a generated `ServerRequest` instance as returned from `Laminas\Diactoros\ServerRequestFactory::fromGlobals()`. +Common use cases include: + +- Generating and injecting a request ID. +- Modifying the request URI based on headers provided (e.g., based on the `X-Forwarded-Host` or `X-Forwarded-Proto` headers). + +## RequestFilterInterface + +A request filter implements `Laminas\Diactoros\RequestFilter\RequestFilterInterface`: + +```php +namespace Laminas\Diactoros\RequestFilter; + +use Psr\Http\Message\ServerRequestInterface; + +interface RequestFilterInterface +{ + public function filterRequest(ServerRequestInterface $request): ServerRequestInterface; +} +``` + +## Implementations + +We provide the following implementations: + +- `NoOpRequestFilter`: returns the provided `$request` verbatim. +- `LegacyXForwardedHeaderFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instanct that reflects those headers. + +### LegacyXForwardedHeaderFilter + +Servers behind a reverse proxy need mechanisms to determine the original URL requested. +As such, reverse proxies have provided a number of mechanisms for delivering this information, with the use of `X-Forwarded-*` headers being the most prevalant. +These include: + +- `X-Forwarded-Host`: the original `Host` header value. +- `X-Forwarded-Port`: the original port included in the `Host` header value. +- `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). + +`Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter` provides mechanisms for accepting these headers and using them to modify the URI composed in the request instance to match the original request. +These methods are: + +- `trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. +- `trustProxies(string|string[] $proxies, string[] $trustedHeaders = LegacyXForwardedHeaderFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. + +Order of operations matters when configuring the instance. +If `trustAny()` is called after `trustProxies()`, the filter will trust any request. +If `trustProxies()` is called after `trustAny()`, the filter will trust only the proxy/ies provided to `trustProxies()`. + +When providing one or more proxies to `trustProxies()`, the values may be exact IP addresses, or subnets specified by [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). +Internally, the filter checks the `REMOTE_ADDR` server parameter (as retrieved from `getServerParams()`) and compares it against each proxy listed; the first to match indicates trust. + +#### Constants + +The `LegacyXForwardedHeaderFilter` defines the following constants for use in specifying various headers: + +- `HEADER_HOST`: corresponds to `X-Forwarded-Host`. +- `HEADER_PORT`: corresponds to `X-Forwarded-Port`. +- `HEADER_PROTO`: corresponds to `X-Forwarded-Proto`. +- `X_FORWARDED_HEADERS`: corresponds to an array consisting of all of the above costants. + +#### Example usage + +Trusting all `X-Forwarded-*` headers from any source: + +```php +$filter = new LegacyXForwardedHeaderFilter(); +$filter->trustAny(); +``` + +Trusting only the `X-Forwarded-Host` header from any source: + +```php +$filter = new LegacyXForwardedHeaderFilter(); +$filter->trustProxies('0.0.0.0/0', [LegacyXForwardedHeaderFilter::HEADER_HOST]); +``` + +Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a Class C subnet: + +```php +$filter = new LegacyXForwardedHeaderFilter(); +$filter->trustProxies( + '192.168.1.0/24', + [LegacyXForwardedHeaderFilter::HEADER_HOST, LegacyXForwardedHeaderFilter::HEADER_PROTO] +); +``` + +Trusting the `X-Forwarded-Host` header from either a Class A or a Class C subnet: + +```php +$filter = new LegacyXForwardedHeaderFilter(); +$filter->trustProxies( + ['10.1.1.0/16', '192.168.1.0/24'], + [LegacyXForwardedHeaderFilter::HEADER_HOST, LegacyXForwardedHeaderFilter::HEADER_PROTO] +); +``` diff --git a/mkdocs.yml b/mkdocs.yml index cc351e7a..68170874 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,11 +14,13 @@ nav: - Usage: v2/usage.md - Reference: - Factories: v2/factories.md + - "Request Filters": v2/request-filters.md - "Custom Responses": v2/custom-responses.md - Serialization: v2/serialization.md - API: v2/api.md - Migration: - "Migration to Version 2": v2/migration.md + - "Preparing for Version 3": v2/forward-migration.md - v1: - Overview: v1/overview.md - Installation: v1/install.md diff --git a/src/RequestFilter/LegacyXForwardedHeaderFilter.php b/src/RequestFilter/LegacyXForwardedHeaderFilter.php index a3aedbf9..897ea144 100644 --- a/src/RequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/RequestFilter/LegacyXForwardedHeaderFilter.php @@ -10,9 +10,9 @@ final class LegacyXForwardedHeaderFilter implements RequestFilterInterface { - public const HEADER_HOST = 'X-FORWARDED-HOST'; - public const HEADER_PORT = 'X-FORWARDED-PORT'; - public const HEADER_PROTO = 'X-FORWARDED-PROTO'; + public const HEADER_HOST = 'X-FORWARDED-HOST'; + public const HEADER_PORT = 'X-FORWARDED-PORT'; + public const HEADER_PROTO = 'X-FORWARDED-PROTO'; public const X_FORWARDED_HEADERS = [ self::HEADER_HOST, diff --git a/src/functions/marshal_uri_from_sapi_safely.php b/src/functions/marshal_uri_from_sapi_safely.php index c80be9e8..63a65458 100644 --- a/src/functions/marshal_uri_from_sapi_safely.php +++ b/src/functions/marshal_uri_from_sapi_safely.php @@ -145,12 +145,8 @@ function marshalUriFromSapiSafely(array $server, array $headers) : Uri $https = false; } - if ($https - || strtolower($getHeaderFromArray('x-forwarded-proto', $headers, '')) === 'https' - ) { - $scheme = 'https'; - } - $uri = $uri->withScheme($scheme); + $scheme = $https ? 'https' : $scheme; + $uri = $uri->withScheme($scheme); // Set the host [$host, $port] = $marshalHostAndPort($server); From 9c48e64d68c7e07db3a97461ff72d77f3e469a5e Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 13:25:29 -0500 Subject: [PATCH 03/45] qa: adapt to work under PHP 7.3 Version 2.11.0 targetted PHP 7.3. Since our security window says we apply to current release branch, we need to target 7.3 to ensure that all existing users can update to a secure version. Signed-off-by: Matthew Weier O'Phinney --- composer.json | 4 +- composer.lock | 359 +++++++++--------- .../LegacyXForwardedHeaderFilter.php | 14 +- test/ServerRequestFactoryTest.php | 3 +- 4 files changed, 197 insertions(+), 183 deletions(-) diff --git a/composer.json b/composer.json index 2ebdfa75..a9e6fc2b 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "config": { "sort-packages": true, "platform": { - "php": "7.4.99" + "php": "7.3.99" } }, "extra": { @@ -31,7 +31,7 @@ } }, "require": { - "php": "^7.4 || ~8.0.0 || ~8.1.0", + "php": "^7.3 || ~8.0.0 || ~8.1.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.0" }, diff --git a/composer.lock b/composer.lock index 6f00fc02..738c3e55 100644 --- a/composer.lock +++ b/composer.lock @@ -357,30 +357,30 @@ }, { "name": "composer/pcre", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" + "reference": "c8e9d27cfc5ed22643c19c160455b473ffd8aabe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "url": "https://api.github.com/repos/composer/pcre/zipball/c8e9d27cfc5ed22643c19c160455b473ffd8aabe", + "reference": "c8e9d27cfc5ed22643c19c160455b473ffd8aabe", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^1.3", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "2.x-dev" } }, "autoload": { @@ -408,7 +408,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.1" + "source": "https://github.com/composer/pcre/tree/2.0.0" }, "funding": [ { @@ -424,20 +424,20 @@ "type": "tidelift" } ], - "time": "2022-01-21T20:24:37+00:00" + "time": "2022-02-25T20:05:29+00:00" }, { "name": "composer/semver", - "version": "3.2.9", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649" + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/a951f614bd64dcd26137bc9b7b2637ddcfc57649", - "reference": "a951f614bd64dcd26137bc9b7b2637ddcfc57649", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", "shasum": "" }, "require": { @@ -489,7 +489,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.9" + "source": "https://github.com/composer/semver/tree/3.3.2" }, "funding": [ { @@ -505,24 +505,24 @@ "type": "tidelift" } ], - "time": "2022-02-04T13:58:43+00:00" + "time": "2022-04-01T19:23:25+00:00" }, { "name": "composer/xdebug-handler", - "version": "3.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "12f1b79476638a5615ed00ea6adbb269cec96fd8" + "reference": "ced299686f41dce890debac69273b47ffe98a40c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/12f1b79476638a5615ed00ea6adbb269cec96fd8", - "reference": "12f1b79476638a5615ed00ea6adbb269cec96fd8", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", "shasum": "" }, "require": { - "composer/pcre": "^1", + "composer/pcre": "^1 || ^2 || ^3", "php": "^7.2.5 || ^8.0", "psr/log": "^1 || ^2 || ^3" }, @@ -555,7 +555,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.1" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" }, "funding": [ { @@ -571,7 +571,7 @@ "type": "tidelift" } ], - "time": "2022-01-04T18:29:42+00:00" + "time": "2022-02-25T21:32:43+00:00" }, { "name": "dnoegel/php-xdg-base-dir", @@ -612,29 +612,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^8.0", + "doctrine/coding-standard": "^9", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" }, "type": "library", "autoload": { @@ -661,7 +662,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" }, "funding": [ { @@ -677,7 +678,7 @@ "type": "tidelift" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2022-03-03T08:28:38+00:00" }, { "name": "felixfbecker/advanced-json-rpc", @@ -726,16 +727,16 @@ }, { "name": "felixfbecker/language-server-protocol", - "version": "1.5.1", + "version": "v1.5.2", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730" + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730", - "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", "shasum": "" }, "require": { @@ -776,9 +777,9 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" }, - "time": "2021-02-22T14:02:09+00:00" + "time": "2022-03-02T22:36:06+00:00" }, { "name": "http-interop/http-factory-tests", @@ -935,25 +936,29 @@ }, { "name": "myclabs/deep-copy", - "version": "1.10.2", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { @@ -978,7 +983,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" }, "funding": [ { @@ -986,7 +991,7 @@ "type": "tidelift" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2022-03-03T13:19:32+00:00" }, { "name": "netresearch/jsonmapper", @@ -1041,16 +1046,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v4.14.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", "shasum": "" }, "require": { @@ -1091,9 +1096,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" }, - "time": "2021-11-30T19:35:32+00:00" + "time": "2022-05-31T20:59:12+00:00" }, { "name": "openlss/lib-array2xml", @@ -1430,16 +1435,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.0", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" + "reference": "77a32518733312af16a44300404e945338981de3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", - "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", + "reference": "77a32518733312af16a44300404e945338981de3", "shasum": "" }, "require": { @@ -1474,9 +1479,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" }, - "time": "2022-01-04T19:58:01+00:00" + "time": "2022-03-15T21:29:03+00:00" }, { "name": "phpspec/prophecy", @@ -1599,16 +1604,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.11", + "version": "9.2.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f" + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/665a1ac0a763c51afc30d6d130dac0813092b17f", - "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", "shasum": "" }, "require": { @@ -1664,7 +1669,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" }, "funding": [ { @@ -1672,7 +1677,7 @@ "type": "github" } ], - "time": "2022-02-18T12:46:09+00:00" + "time": "2022-03-07T09:28:20+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1917,16 +1922,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.14", + "version": "9.5.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1883687169c017d6ae37c58883ca3994cfc34189" + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1883687169c017d6ae37c58883ca3994cfc34189", - "reference": "1883687169c017d6ae37c58883ca3994cfc34189", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", "shasum": "" }, "require": { @@ -1942,7 +1947,7 @@ "phar-io/version": "^3.0.2", "php": ">=7.3", "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.7", + "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -1956,11 +1961,10 @@ "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3.4", + "sebastian/type": "^3.0", "sebastian/version": "^3.0.2" }, "require-dev": { - "ext-pdo": "*", "phpspec/prophecy-phpunit": "^2.0.1" }, "suggest": { @@ -2004,7 +2008,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.14" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21" }, "funding": [ { @@ -2016,7 +2020,7 @@ "type": "github" } ], - "time": "2022-02-18T12:54:07+00:00" + "time": "2022-06-19T12:14:25+00:00" }, { "name": "psalm/plugin-phpunit", @@ -2542,16 +2546,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", "shasum": "" }, "require": { @@ -2593,7 +2597,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" }, "funding": [ { @@ -2601,7 +2605,7 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2022-04-03T09:37:03+00:00" }, { "name": "sebastian/exporter", @@ -3033,28 +3037,28 @@ }, { "name": "sebastian/type", - "version": "2.3.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", - "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3077,7 +3081,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" + "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" }, "funding": [ { @@ -3085,7 +3089,7 @@ "type": "github" } ], - "time": "2021-06-15T12:49:02+00:00" + "time": "2022-03-15T09:54:48+00:00" }, { "name": "sebastian/version", @@ -3225,16 +3229,16 @@ }, { "name": "symfony/console", - "version": "v5.4.3", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8" + "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a2a86ec353d825c75856c6fd14fac416a7bdb6b8", - "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8", + "url": "https://api.github.com/repos/symfony/console/zipball/829d5d1bf60b2efeb0887b7436873becc71a45eb", + "reference": "829d5d1bf60b2efeb0887b7436873becc71a45eb", "shasum": "" }, "require": { @@ -3304,7 +3308,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.3" + "source": "https://github.com/symfony/console/tree/v5.4.9" }, "funding": [ { @@ -3320,20 +3324,20 @@ "type": "tidelift" } ], - "time": "2022-01-26T16:28:35+00:00" + "time": "2022-05-18T06:17:34+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -3371,7 +3375,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" }, "funding": [ { @@ -3387,20 +3391,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { @@ -3415,7 +3419,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3423,12 +3427,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3453,7 +3457,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" }, "funding": [ { @@ -3469,20 +3473,20 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + "reference": "433d05519ce6990bf3530fba6957499d327395c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", - "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", "shasum": "" }, "require": { @@ -3494,7 +3498,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3534,7 +3538,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" }, "funding": [ { @@ -3550,20 +3554,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T21:10:46+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -3575,7 +3579,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3618,7 +3622,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" }, "funding": [ { @@ -3634,20 +3638,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { @@ -3662,7 +3666,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3701,7 +3705,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" }, "funding": [ { @@ -3717,20 +3721,20 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", "shasum": "" }, "require": { @@ -3739,7 +3743,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3780,7 +3784,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" }, "funding": [ { @@ -3796,20 +3800,20 @@ "type": "tidelift" } ], - "time": "2021-06-05T21:20:04+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", - "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "shasum": "" }, "require": { @@ -3818,7 +3822,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3863,7 +3867,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" }, "funding": [ { @@ -3879,26 +3883,26 @@ "type": "tidelift" } ], - "time": "2021-09-13T13:58:33+00:00" + "time": "2022-05-10T07:21:04+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -3946,7 +3950,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" }, "funding": [ { @@ -3962,20 +3966,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2022-03-13T20:07:29+00:00" }, { "name": "symfony/string", - "version": "v5.4.3", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10" + "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/92043b7d8383e48104e411bc9434b260dbeb5a10", - "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10", + "url": "https://api.github.com/repos/symfony/string/zipball/985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", + "reference": "985e6a9703ef5ce32ba617c9c7d97873bb7b2a99", "shasum": "" }, "require": { @@ -3997,12 +4001,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -4032,7 +4036,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.3" + "source": "https://github.com/symfony/string/tree/v5.4.9" }, "funding": [ { @@ -4048,7 +4052,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2022-04-19T10:40:37+00:00" }, { "name": "theseer/tokenizer", @@ -4102,16 +4106,16 @@ }, { "name": "vimeo/psalm", - "version": "4.21.0", + "version": "4.23.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d8bec4c7aaee111a532daec32fb09de5687053d1" + "reference": "f1fe6ff483bf325c803df9f510d09a03fd796f88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d8bec4c7aaee111a532daec32fb09de5687053d1", - "reference": "d8bec4c7aaee111a532daec32fb09de5687053d1", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/f1fe6ff483bf325c803df9f510d09a03fd796f88", + "reference": "f1fe6ff483bf325c803df9f510d09a03fd796f88", "shasum": "" }, "require": { @@ -4136,6 +4140,7 @@ "php": "^7.1|^8", "sebastian/diff": "^3.0 || ^4.0", "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.25", "webmozart/path-util": "^2.3" }, "provide": { @@ -4202,27 +4207,27 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.21.0" + "source": "https://github.com/vimeo/psalm/tree/4.23.0" }, - "time": "2022-02-18T04:34:15+00:00" + "time": "2022-04-28T17:35:49+00:00" }, { "name": "webmozart/assert", - "version": "1.10.0", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" + "ext-ctype": "*", + "php": "^7.2 || ^8.0" }, "conflict": { "phpstan/phpstan": "<0.12.20", @@ -4260,9 +4265,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" + "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2022-06-03T18:03:27+00:00" }, { "name": "webmozart/path-util", diff --git a/src/RequestFilter/LegacyXForwardedHeaderFilter.php b/src/RequestFilter/LegacyXForwardedHeaderFilter.php index 897ea144..5a03ece9 100644 --- a/src/RequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/RequestFilter/LegacyXForwardedHeaderFilter.php @@ -22,10 +22,18 @@ final class LegacyXForwardedHeaderFilter implements RequestFilterInterface /** * @todo Toggle this to false for version 3.0. + * @var bool */ - private bool $trustAny = true; - private array $trustedHeaders = []; - private array $trustedProxies = []; + private $trustAny = true; + + /** + * @var string[] + * @psalm-var array + */ + private $trustedHeaders = []; + + /** @var string[] */ + private $trustedProxies = []; // public function filterRequest(array $headers, string $remoteAddress): array public function filterRequest(ServerRequestInterface $request): ServerRequestInterface diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index 74a37cd0..b7da02c9 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -729,7 +729,8 @@ public function testReturnsFilteredRequestBasedOnRequestFilterProvided(): void { $expectedRequest = new ServerRequest(); $filter = new class($expectedRequest) implements RequestFilterInterface { - private ServerRequestInterface $request; + /** @var ServerRequestInterface */ + private $request; public function __construct(ServerRequestInterface $request) { From 37137babf8da7a7788805bea29a32a763ac37777 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 16:08:00 -0500 Subject: [PATCH 04/45] refactor: rename namespace and interface to ServerRequestFilter This will allow us to add (client) request filters later if desired. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/api.md | 8 ++++---- docs/book/v2/forward-migration.md | 10 +++++----- ...equest-filters.md => server-request-filters.md} | 14 +++++++------- mkdocs.yml | 2 +- src/ServerRequestFactory.php | 8 ++++---- .../IPRange.php | 2 +- .../LegacyXForwardedHeaderFilter.php | 4 ++-- .../NoOpRequestFilter.php | 4 ++-- .../ServerRequestFilterInterface.php} | 4 ++-- test/ServerRequestFactoryTest.php | 6 ++---- .../IPRangeTest.php | 4 ++-- .../LegacyXForwardedHeaderFilterTest.php | 4 ++-- .../NoOpRequestFilterTest.php | 4 ++-- 13 files changed, 36 insertions(+), 38 deletions(-) rename docs/book/v2/{request-filters.md => server-request-filters.md} (85%) rename src/{RequestFilter => ServerRequestFilter}/IPRange.php (97%) rename src/{RequestFilter => ServerRequestFilter}/LegacyXForwardedHeaderFilter.php (97%) rename src/{RequestFilter => ServerRequestFilter}/NoOpRequestFilter.php (64%) rename src/{RequestFilter/RequestFilterInterface.php => ServerRequestFilter/ServerRequestFilterInterface.php} (66%) rename test/{RequestFilter => ServerRequestFilter}/IPRangeTest.php (97%) rename test/{RequestFilter => ServerRequestFilter}/LegacyXForwardedHeaderFilterTest.php (98%) rename test/{RequestFilter => ServerRequestFilter}/NoOpRequestFilterTest.php (77%) diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index 1ed558d4..0a1d603c 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -125,11 +125,11 @@ $request = ServerRequestFactory::fromGlobals( $_FILES ); -### Request Filter +### Request Filters Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. -This should be a `null` value, or an instance of [`Laminas\Diactoros\RequestFilter\RequestFilterInterface`](request-filters.md). -For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter`](request-filters.md#legacyxforwardedheaderfilter) instance configured as follows: +This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface`](server-request-filters.md). +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter`](server-request-filters.md#legacyxforwardedheaderfilter) instance configured as follows: ```php $requestFilter = new LegacyXForwardedHeaderFilter(); @@ -138,7 +138,7 @@ $requestFilter->trustAny(); The request filter is called on the generated server request instance, and its result is returned from `fromGlobals()`. -**For version 3 releases, this method will switch to using a `Laminas\Diactoros\RequestFilter\NoOpRequestFilter` by default.** +**For version 3 releases, this method will switch to using a `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` by default.** If you are using this factory method directly, please be aware and update your code accordingly. ### ServerRequestFactory Helper Functions diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md index 770c9405..4ebbe4a5 100644 --- a/docs/book/v2/forward-migration.md +++ b/docs/book/v2/forward-migration.md @@ -1,13 +1,13 @@ # Preparing for Version 3 -## RequestFilterInterface defaults +## ServerRequestFilterInterface defaults -Introduced in version 2.11.1, the `Laminas\Diactoros\RequestFilter\RequestFilterInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. +Introduced in version 2.11.1, the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. The primary use case is to allow modifying the generated URI based on the presence of headers such as `X-Forwarded-Host`. When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. (We have also traditionally examined the `X-Forwarded-Proto` header; some implementations examine the `X-Forwarded-Port` header as well.) -To accommodate this use case, we created `Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter`. +To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter`. (The "Legacy" verbiage is because a [new RFC (7239)](https://datatracker.ietf.org/doc/html/rfc7239) provides an official specification for this behavior via a new `Forwarded` header.) Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. @@ -16,7 +16,7 @@ Due to potential security issues, it is generally best to only accept these head To prevent backwards compatibility breaks, we use this filter by default, marked to trust any proxy. However, **in version 3, we will use a no-op filter by default**. -Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\RequestFilter\RequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `LegacyXForwardedHeaderFilter` if you depend on this functionality. -If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\RequestFilter\NoOpRequestFilter` as the configured `RequestFilterInterface` in your application immediately. +Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `LegacyXForwardedHeaderFilter` if you depend on this functionality. +If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` as the configured `ServerRequestFilterInterface` in your application immediately. We will update this documentation with a link to the related functionality in mezzio/mezzio when it is published. diff --git a/docs/book/v2/request-filters.md b/docs/book/v2/server-request-filters.md similarity index 85% rename from docs/book/v2/request-filters.md rename to docs/book/v2/server-request-filters.md index 79804e55..78f0005c 100644 --- a/docs/book/v2/request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -1,23 +1,23 @@ -# Request Filters +# Server Request Filters > - Since laminas/laminas-diactoros 2.11.1 -Request filters allow you to modify the initial state of a generated `ServerRequest` instance as returned from `Laminas\Diactoros\ServerRequestFactory::fromGlobals()`. +Server request filters allow you to modify the initial state of a generated `ServerRequest` instance as returned from `Laminas\Diactoros\ServerRequestFactory::fromGlobals()`. Common use cases include: - Generating and injecting a request ID. - Modifying the request URI based on headers provided (e.g., based on the `X-Forwarded-Host` or `X-Forwarded-Proto` headers). -## RequestFilterInterface +## ServerRequestFilterInterface -A request filter implements `Laminas\Diactoros\RequestFilter\RequestFilterInterface`: +A request filter implements `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface`: ```php -namespace Laminas\Diactoros\RequestFilter; +namespace Laminas\Diactoros\ServerRequestFilter; use Psr\Http\Message\ServerRequestInterface; -interface RequestFilterInterface +interface ServerRequestFilterInterface { public function filterRequest(ServerRequestInterface $request): ServerRequestInterface; } @@ -40,7 +40,7 @@ These include: - `X-Forwarded-Port`: the original port included in the `Host` header value. - `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). -`Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter` provides mechanisms for accepting these headers and using them to modify the URI composed in the request instance to match the original request. +`Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter` provides mechanisms for accepting these headers and using them to modify the URI composed in the request instance to match the original request. These methods are: - `trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. diff --git a/mkdocs.yml b/mkdocs.yml index 68170874..7d41499c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,7 @@ nav: - Usage: v2/usage.md - Reference: - Factories: v2/factories.md - - "Request Filters": v2/request-filters.md + - "Server Request Filters": v2/server-request-filters.md - "Custom Responses": v2/custom-responses.md - Serialization: v2/serialization.md - API: v2/api.md diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index f1f52f4f..e44c0ca3 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -4,8 +4,8 @@ namespace Laminas\Diactoros; -use Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter; -use Laminas\Diactoros\RequestFilter\RequestFilterInterface; +use Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -44,7 +44,7 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * @param array $body $_POST superglobal * @param array $cookies $_COOKIE superglobal * @param array $files $_FILES superglobal - * @param null|RequestFilterInterface $requestFilter If present, the + * @param null|ServerRequestFilterInterface $requestFilter If present, the * generated request will be passed to this instance and the result * returned by this method. When not present, a default instance * is created and used. For version 2, that instance is of @@ -58,7 +58,7 @@ public static function fromGlobals( array $body = null, array $cookies = null, array $files = null, - ?RequestFilterInterface $requestFilter = null + ?ServerRequestFilterInterface $requestFilter = null ) : ServerRequest { // @todo For version 3, we should instead create a NoOpRequestFilter instance. if (null === $requestFilter) { diff --git a/src/RequestFilter/IPRange.php b/src/ServerRequestFilter/IPRange.php similarity index 97% rename from src/RequestFilter/IPRange.php rename to src/ServerRequestFilter/IPRange.php index 837c29d4..868cf80f 100644 --- a/src/RequestFilter/IPRange.php +++ b/src/ServerRequestFilter/IPRange.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\Diactoros\RequestFilter; +namespace Laminas\Diactoros\ServerRequestFilter; final class IPRange { diff --git a/src/RequestFilter/LegacyXForwardedHeaderFilter.php b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php similarity index 97% rename from src/RequestFilter/LegacyXForwardedHeaderFilter.php rename to src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php index 5a03ece9..36e20c94 100644 --- a/src/RequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Laminas\Diactoros\RequestFilter; +namespace Laminas\Diactoros\ServerRequestFilter; use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException; use Laminas\Diactoros\Exception\InvalidProxyAddressException; use Psr\Http\Message\ServerRequestInterface; -final class LegacyXForwardedHeaderFilter implements RequestFilterInterface +final class LegacyXForwardedHeaderFilter implements ServerRequestFilterInterface { public const HEADER_HOST = 'X-FORWARDED-HOST'; public const HEADER_PORT = 'X-FORWARDED-PORT'; diff --git a/src/RequestFilter/NoOpRequestFilter.php b/src/ServerRequestFilter/NoOpRequestFilter.php similarity index 64% rename from src/RequestFilter/NoOpRequestFilter.php rename to src/ServerRequestFilter/NoOpRequestFilter.php index 0a162b97..2ba0d6cf 100644 --- a/src/RequestFilter/NoOpRequestFilter.php +++ b/src/ServerRequestFilter/NoOpRequestFilter.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Laminas\Diactoros\RequestFilter; +namespace Laminas\Diactoros\ServerRequestFilter; use Psr\Http\Message\ServerRequestInterface; -final class NoOpRequestFilter implements RequestFilterInterface +final class NoOpRequestFilter implements ServerRequestFilterInterface { public function filterRequest(ServerRequestInterface $request): ServerRequestInterface { diff --git a/src/RequestFilter/RequestFilterInterface.php b/src/ServerRequestFilter/ServerRequestFilterInterface.php similarity index 66% rename from src/RequestFilter/RequestFilterInterface.php rename to src/ServerRequestFilter/ServerRequestFilterInterface.php index eb1528b4..a4383dcd 100644 --- a/src/RequestFilter/RequestFilterInterface.php +++ b/src/ServerRequestFilter/ServerRequestFilterInterface.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Laminas\Diactoros\RequestFilter; +namespace Laminas\Diactoros\ServerRequestFilter; use Psr\Http\Message\ServerRequestInterface; -interface RequestFilterInterface +interface ServerRequestFilterInterface { public function filterRequest(ServerRequestInterface $request): ServerRequestInterface; } diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index b7da02c9..fca18d21 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -4,15 +4,13 @@ namespace LaminasTest\Diactoros; -use Laminas\Diactoros\RequestFilter\RequestFilterInterface; use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; +use Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface; use Laminas\Diactoros\UploadedFile; use Laminas\Diactoros\Uri; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; -use ReflectionMethod; -use ReflectionProperty; use UnexpectedValueException; use function Laminas\Diactoros\marshalHeadersFromSapi; @@ -728,7 +726,7 @@ public function testDoesNotMarshalAllContentPrefixedServerVarsAsHeaders( public function testReturnsFilteredRequestBasedOnRequestFilterProvided(): void { $expectedRequest = new ServerRequest(); - $filter = new class($expectedRequest) implements RequestFilterInterface { + $filter = new class($expectedRequest) implements ServerRequestFilterInterface { /** @var ServerRequestInterface */ private $request; diff --git a/test/RequestFilter/IPRangeTest.php b/test/ServerRequestFilter/IPRangeTest.php similarity index 97% rename from test/RequestFilter/IPRangeTest.php rename to test/ServerRequestFilter/IPRangeTest.php index 3cbcfe22..65716ebf 100644 --- a/test/RequestFilter/IPRangeTest.php +++ b/test/ServerRequestFilter/IPRangeTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace LaminasTest\Diactoros\RequestFilter; +namespace LaminasTest\Diactoros\ServerRequestFilter; -use Laminas\Diactoros\RequestFilter\IPRange; +use Laminas\Diactoros\ServerRequestFilter\IPRange; use PHPUnit\Framework\TestCase; class IPRangeTest extends TestCase diff --git a/test/RequestFilter/LegacyXForwardedHeaderFilterTest.php b/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php similarity index 98% rename from test/RequestFilter/LegacyXForwardedHeaderFilterTest.php rename to test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php index 7218561d..554e327b 100644 --- a/test/RequestFilter/LegacyXForwardedHeaderFilterTest.php +++ b/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace LaminasTest\Diactoros\RequestFilter; +namespace LaminasTest\Diactoros\ServerRequestFilter; use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException; use Laminas\Diactoros\Exception\InvalidProxyAddressException; -use Laminas\Diactoros\RequestFilter\LegacyXForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter; use Laminas\Diactoros\ServerRequest; use PHPUnit\Framework\TestCase; diff --git a/test/RequestFilter/NoOpRequestFilterTest.php b/test/ServerRequestFilter/NoOpRequestFilterTest.php similarity index 77% rename from test/RequestFilter/NoOpRequestFilterTest.php rename to test/ServerRequestFilter/NoOpRequestFilterTest.php index 2a3c19eb..f6cf884d 100644 --- a/test/RequestFilter/NoOpRequestFilterTest.php +++ b/test/ServerRequestFilter/NoOpRequestFilterTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace LaminasTest\Diactoros\RequestFilter; +namespace LaminasTest\Diactoros\ServerRequestFilter; -use Laminas\Diactoros\RequestFilter\NoOpRequestFilter; +use Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter; use Laminas\Diactoros\ServerRequest; use PHPUnit\Framework\TestCase; From f544f55615fdc3c2286fa08305cf59aa03541b11 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 17:21:35 -0500 Subject: [PATCH 05/45] fix: remove unused private method Signed-off-by: Matthew Weier O'Phinney --- .../LegacyXForwardedHeaderFilter.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php index 36e20c94..ca69632d 100644 --- a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php @@ -194,19 +194,4 @@ private function validateProxyCIDR($cidr): bool ) ); } - - private function getHeaderListToRemove(): array - { - $toRemove = self::X_FORWARDED_HEADERS; - foreach ($this->trustedHeaders as $header) { - $index = array_search($header, $toRemove, true); - if (! is_int($index)) { - continue; - } - - unset($toRemove[$index]); - } - - return $toRemove; - } } From c87bae19221b5eaa86765620aac42840d0f07cc2 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 17:22:08 -0500 Subject: [PATCH 06/45] fix: add @throws annotation to method Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php index ca69632d..14bb7378 100644 --- a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php @@ -138,6 +138,7 @@ private function validateTrustedHeaders(array $headers): void } } + /** @throws InvalidProxyAddressException */ private function normalizeProxiesList($proxies): array { if (! is_array($proxies) && ! is_string($proxies)) { From a68f88db3fa72de88bd5f5dd34e6eeb9b8c9eb21 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 17:27:54 -0500 Subject: [PATCH 07/45] fix: do not rewrite URI values if a list is present If we have a list, there's no way to know which is the correct one. Signed-off-by: Matthew Weier O'Phinney --- .../LegacyXForwardedHeaderFilter.php | 3 +- .../LegacyXForwardedHeaderFilterTest.php | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php index 14bb7378..8b1bd857 100644 --- a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php @@ -54,7 +54,8 @@ public function filterRequest(ServerRequestInterface $request): ServerRequestInt $uri = $originalUri = $request->getUri(); foreach ($this->trustedHeaders as $headerName) { $header = $request->getHeaderLine($headerName); - if ('' === $header) { + if ('' === $header || false !== strpos($header, ',')) { + // Reject empty headers and/or headers with multiple values continue; } diff --git a/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php b/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php index 554e327b..8812084d 100644 --- a/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php +++ b/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php @@ -210,4 +210,64 @@ public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesExc $this->expectException(InvalidForwardedHeaderNameException::class); $filter->trustProxies('192.168.1.0/24', ['Host']); } + + public function testListOfForwardedHostsIsConsideredUntrusted(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com,proxy.api.example.com', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustAny(); + + $this->assertSame($request, $filter->filterRequest($request)); + } + + public function testListOfForwardedPortsIsConsideredUntrusted(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Port' => '8080,9000', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustAny(); + + $this->assertSame($request, $filter->filterRequest($request)); + } + + public function testListOfForwardedProtosIsConsideredUntrusted(): void + { + $request = new ServerRequest( + ['REMOTE_ADDR' => '192.168.1.1'], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Proto' => 'http,https', + ] + ); + + $filter = new LegacyXForwardedHeaderFilter(); + $filter->trustAny(); + + $this->assertSame($request, $filter->filterRequest($request)); + } } From e4daab477a2e7f69aaa19585d3e06cb59553ec76 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 17:29:34 -0500 Subject: [PATCH 08/45] docs: ensure we detail that we now examine X-Forwarded-Port as well Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/forward-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md index 4ebbe4a5..80b98f9c 100644 --- a/docs/book/v2/forward-migration.md +++ b/docs/book/v2/forward-migration.md @@ -5,7 +5,7 @@ Introduced in version 2.11.1, the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. The primary use case is to allow modifying the generated URI based on the presence of headers such as `X-Forwarded-Host`. When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. -(We have also traditionally examined the `X-Forwarded-Proto` header; some implementations examine the `X-Forwarded-Port` header as well.) +(We have always examined the `X-Forwarded-Proto` header; as of 2.11.1, we also examine the `X-Forwarded-Port` header.) To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter`. (The "Legacy" verbiage is because a [new RFC (7239)](https://datatracker.ietf.org/doc/html/rfc7239) provides an official specification for this behavior via a new `Forwarded` header.) From 257cfd564b85adfd9ff85ffed2dd7eb1556ea4ea Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 17:30:36 -0500 Subject: [PATCH 09/45] docs: be explicit that trustAny() trusts ANY of the x-forwarded headers Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php index 8b1bd857..2b7f9c73 100644 --- a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php @@ -82,7 +82,7 @@ public function filterRequest(ServerRequestInterface $request): ServerRequestInt } /** - * Trust X-FORWARDED-* headers from any address. + * Trust any X-FORWARDED-* headers from any address. * * WARNING: Only do this if you know for certain that your application * sits behind a trusted proxy that cannot be spoofed. This should only From 64234fad6c4db97de1655f935643e3f09eb11187 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 21 Jun 2022 17:31:34 -0500 Subject: [PATCH 10/45] docs: fixes typo Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 78f0005c..ae9ecec0 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -28,7 +28,7 @@ interface ServerRequestFilterInterface We provide the following implementations: - `NoOpRequestFilter`: returns the provided `$request` verbatim. -- `LegacyXForwardedHeaderFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instanct that reflects those headers. +- `LegacyXForwardedHeaderFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. ### LegacyXForwardedHeaderFilter From be0c15a112fe516e8fc08937140f26c39b1b9b4c Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 22 Jun 2022 17:21:24 -0500 Subject: [PATCH 11/45] feat: add PSR-11 factory for LegacyXForwardedHeaderFilter This patch adds a PSR-11 factory for the LegacyXForwardedHeaderFilter via the LegacyXForwardedHeaderFilterFactory class. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 37 ++ src/ConfigProvider.php | 21 + .../LegacyXForwardedHeaderFilterFactory.php | 57 +++ ...egacyXForwardedHeaderFilterFactoryTest.php | 402 ++++++++++++++++++ 4 files changed, 517 insertions(+) create mode 100644 src/ServerRequestFilter/LegacyXForwardedHeaderFilterFactory.php create mode 100644 test/ServerRequestFilter/LegacyXForwardedHeaderFilterFactoryTest.php diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index ae9ecec0..6d8cbcd9 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -97,3 +97,40 @@ $filter->trustProxies( [LegacyXForwardedHeaderFilter::HEADER_HOST, LegacyXForwardedHeaderFilter::HEADER_PROTO] ); ``` + +#### LegacyXForwardedHeaderFilterFactory + +Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter` via the `Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilterFactory` class. +This factory looks for the following configuration in order to generate an instance: + +```php +$config = [ + 'laminas-diactoros' => [ + 'legacy-x-forwarded-header-filter' => [ + 'trust-any' => bool, + 'trusted-proxies' => string|string[], + 'trusted-headers' => string[], + ], + ], +]; +``` + +- The `trust-any` key should be a boolean. + By default, it is `false`; toggling it `true` will cause the `trustAny()` method to be called on the generated instance. + This flag overrides the `trusted-proxies` configuration. +- The `trusted-proxies` array should be a string IP address or CIDR notation, or an array of such values, each indicating a trusted proxy server or subnet of such servers. +- The `trusted-headers` array should consist of one or more of the `X-Forwarded-Host`, `X-Forwarded-Port`, or `X-Forwarded-Proto` header names; the values are case insensitive. + When the configuration is omitted or the array is empty, the assumption is to honor all `X-Forwarded-*` headers for trusted proxies. + +Register the factory using the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` key: + +```php +$config = [ + 'dependencies' => [ + 'factories' => [ + \Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface::class => + \Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilterFactory::class, + ], + ], +]; +``` diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 32510d1f..70d5fdc1 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -13,6 +13,12 @@ class ConfigProvider { + public const CONFIG_KEY = 'laminas-diactoros'; + public const LEGACY_X_FORWARDED = 'laminas-x-forwarded-header-filter'; + public const LEGACY_X_FORWARDED_TRUST_ANY = 'trust-any'; + public const LEGACY_X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies'; + public const LEGACY_X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers'; + /** * Retrieve configuration for laminas-diactoros. * @@ -22,6 +28,7 @@ public function __invoke() : array { return [ 'dependencies' => $this->getDependencies(), + self::CONFIG_KEY => $this->getComponentConfig(), ]; } @@ -31,15 +38,29 @@ public function __invoke() : array */ public function getDependencies() : array { + // @codingStandardsIgnoreStart return [ 'invokables' => [ RequestFactoryInterface::class => RequestFactory::class, ResponseFactoryInterface::class => ResponseFactory::class, StreamFactoryInterface::class => StreamFactory::class, ServerRequestFactoryInterface::class => ServerRequestFactory::class, + ServerRequestFilter\LegacyXForwardedHeaderFilter::class => ServerRequestFilter\LegacyXForwardedHeaderFilterFactory::class, UploadedFileFactoryInterface::class => UploadedFileFactory::class, UriFactoryInterface::class => UriFactory::class ], ]; + // @codingStandardsIgnoreEnd + } + + public function getComponentConfig(): array + { + return [ + self::LEGACY_X_FORWARDED => [ + self::LEGACY_X_FORWARDED_TRUST_ANY => false, + self::LEGACY_X_FORWARDED_TRUSTED_PROXIES => [], + self::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [], + ], + ]; } } diff --git a/src/ServerRequestFilter/LegacyXForwardedHeaderFilterFactory.php b/src/ServerRequestFilter/LegacyXForwardedHeaderFilterFactory.php new file mode 100644 index 00000000..a6e07cbd --- /dev/null +++ b/src/ServerRequestFilter/LegacyXForwardedHeaderFilterFactory.php @@ -0,0 +1,57 @@ +get('config'); + $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::LEGACY_X_FORWARDED] ?? []; + + $filter = new LegacyXForwardedHeaderFilter(); + + if (empty($config)) { + return $filter; + } + + if (array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY, $config) + && $config[ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY] + ) { + $filter->trustAny(); + return $filter; + } + + $proxies = array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES, $config) + ? $config[ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES] + : []; + + if ((! is_string($proxies) && ! is_array($proxies)) + || empty($proxies) + ) { + // Makes no sense to set trusted headers if no proxies are trusted + return $filter; + } + + $headers = array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS, $config) + ? $config[ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS] + : LegacyXForwardedHeaderFilter::X_FORWARDED_HEADERS; + + if (! is_array($headers)) { + // Invalid value + return $filter; + } + + // Empty headers list implies trust all + $headers = empty($headers) ? LegacyXForwardedHeaderFilter::X_FORWARDED_HEADERS : $headers; + + $filter->trustProxies($proxies, $headers); + + return $filter; + } +} diff --git a/test/ServerRequestFilter/LegacyXForwardedHeaderFilterFactoryTest.php b/test/ServerRequestFilter/LegacyXForwardedHeaderFilterFactoryTest.php new file mode 100644 index 00000000..a3f7117a --- /dev/null +++ b/test/ServerRequestFilter/LegacyXForwardedHeaderFilterFactoryTest.php @@ -0,0 +1,402 @@ +container = new class() implements ContainerInterface { + private $services = []; + + /** + * @param string $id + * @return bool + */ + public function has($id) + { + return array_key_exists($id, $this->services); + } + + /** + * @param string $id + * @return mixed + */ + public function get($id) + { + if (! array_key_exists($id, $this->services)) { + return null; + } + + return $this->services[$id]; + } + + /** @param mixed $value */ + public function set(string $id, $value): void + { + $this->services[$id] = $value; + } + }; + + $this->container->set('config', []); + } + + public function generateServerRequest(array $headers, array $server, string $baseUrlString): ServerRequest + { + return new ServerRequest($server, [], $baseUrlString, 'GET', 'php://temp', $headers); + } + + /** @psalm-return iterable */ + public function randomIpGenerator(): iterable + { + yield 'class-a' => ['10.1.1.1']; + yield 'class-c' => ['192.168.1.1']; + yield 'localhost' => ['127.0.0.1']; + yield 'public' => ['4.4.4.4']; + } + + /** @dataProvider randomIpGenerator */ + public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(string $remoteAddr): void + { + $factory = new LegacyXForwardedHeaderFilterFactory(); + $filter = $factory($this->container); + $request = $this->generateServerRequest( + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + ], + [ + 'REMOTE_ADDR' => $remoteAddr, + ], + 'http://localhost/foo/bar', + ); + + $filteredRequest = $filter->filterRequest($request); + $this->assertSame($request, $filteredRequest); + } + + /** @psalm-return iterable}> */ + public function trustAnyProvider(): iterable + { + $headers = [ + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + ]; + + foreach ($this->randomIpGenerator() as $name => $arguments) { + $arguments[] = $headers; + yield $name => $arguments; + } + } + + /** @dataProvider trustAnyProvider */ + public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( + string $remoteAddr, + array $headers + ): void { + $headers['Host'] = 'localhost'; + $this->container->set('config', [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => true, + ], + ], + ]); + + $factory = new LegacyXForwardedHeaderFilterFactory(); + $filter = $factory($this->container); + $request = $this->generateServerRequest( + $headers, + ['REMOTE_ADDR' => $remoteAddr], + 'http://localhost/foo/bar', + ); + + $filteredRequest = $filter->filterRequest($request); + $this->assertNotSame($request, $filteredRequest); + + $uri = $filteredRequest->getUri(); + $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); + // Port is always cast to int + $this->assertSame((int) $headers[LegacyXForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); + $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); + } + + /** @dataProvider trustAnyProvider */ + public function testEnabledTrustAnyFlagHasPrecedenceOverTrustedProxiesConfig( + string $remoteAddr, + array $headers + ): void { + $headers['Host'] = 'localhost'; + $this->container->set('config', [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => true, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => [ + '192.168.0.0/24', + ], + ], + ], + ]); + + $factory = new LegacyXForwardedHeaderFilterFactory(); + $filter = $factory($this->container); + $request = $this->generateServerRequest( + $headers, + ['REMOTE_ADDR' => $remoteAddr], + 'http://localhost/foo/bar', + ); + + $filteredRequest = $filter->filterRequest($request); + $this->assertNotSame($request, $filteredRequest); + + $uri = $filteredRequest->getUri(); + $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); + // Port is always cast to int + $this->assertSame((int) $headers[LegacyXForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); + $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); + } + + /** @dataProvider randomIpGenerator */ + public function testEmptyProxiesListDoesNotTrustXForwardedHeaders(string $remoteAddr): void + { + $this->container->set('config', [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => [], + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + LegacyXForwardedHeaderFilter::HEADER_HOST, + ], + ], + ], + ]); + + $factory = new LegacyXForwardedHeaderFilterFactory(); + $filter = $factory($this->container); + $request = $this->generateServerRequest( + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + ], + [ + 'REMOTE_ADDR' => $remoteAddr, + ], + 'http://localhost/foo/bar', + ); + + $filteredRequest = $filter->filterRequest($request); + $this->assertSame($request, $filteredRequest); + } + + /** @dataProvider randomIpGenerator */ + public function testEmptyHeadersListTrustsAllXForwardedHeadersForMatchedProxies(string $remoteAddr): void + { + $this->container->set('config', [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['0.0.0.0/0'], + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [], + ], + ], + ]); + + $factory = new LegacyXForwardedHeaderFilterFactory(); + $filter = $factory($this->container); + $request = $this->generateServerRequest( + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + ], + [ + 'REMOTE_ADDR' => $remoteAddr, + ], + 'http://localhost/foo/bar', + ); + + $filteredRequest = $filter->filterRequest($request); + $this->assertNotSame($request, $filteredRequest); + + $uri = $filteredRequest->getUri(); + $this->assertSame('api.example.com', $uri->getHost()); + $this->assertSame(4443, $uri->getPort()); + $this->assertSame('https', $uri->getScheme()); + } + + /** + * @psalm-return iterable>>, + * 2: array, + * 3: array, + * 4: string, + * 5: string + * }> + */ + public function trustedProxiesAndHeaders(): iterable + { + yield 'string-proxy-single-header' => [ + false, + [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + LegacyXForwardedHeaderFilter::HEADER_HOST, + ], + ], + ], + ], + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + ], + ['REMOTE_ADDR' => '192.168.1.1'], + 'http://localhost/foo/bar', + 'http://api.example.com/foo/bar', + ]; + + yield 'single-proxy-single-header' => [ + false, + [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + LegacyXForwardedHeaderFilter::HEADER_HOST, + ], + ], + ], + ], + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + ], + ['REMOTE_ADDR' => '192.168.1.1'], + 'http://localhost/foo/bar', + 'http://api.example.com/foo/bar', + ]; + + yield 'single-proxy-multi-header' => [ + false, + [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + LegacyXForwardedHeaderFilter::HEADER_HOST, + LegacyXForwardedHeaderFilter::HEADER_PROTO, + ], + ], + ], + ], + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + ], + ['REMOTE_ADDR' => '192.168.1.1'], + 'http://localhost/foo/bar', + 'https://api.example.com/foo/bar', + ]; + + yield 'unmatched-proxy-single-header' => [ + true, + [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + LegacyXForwardedHeaderFilter::HEADER_HOST, + ], + ], + ], + ], + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + ], + ['REMOTE_ADDR' => '192.168.2.1'], + 'http://localhost/foo/bar', + 'http://localhost/foo/bar', + ]; + + yield 'matches-proxy-from-list-single-header' => [ + false, + [ + ConfigProvider::CONFIG_KEY => [ + ConfigProvider::LEGACY_X_FORWARDED => [ + ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], + ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + LegacyXForwardedHeaderFilter::HEADER_HOST, + ], + ], + ], + ], + [ + 'Host' => 'localhost', + LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + ], + ['REMOTE_ADDR' => '192.168.2.1'], + 'http://localhost/foo/bar', + 'http://api.example.com/foo/bar', + ]; + } + + /** @dataProvider trustedProxiesAndHeaders */ + public function testCombinedProxiesAndHeadersDefineTrust( + bool $expectUnfiltered, + array $config, + array $headers, + array $server, + string $baseUriString, + string $expectedUriString + ): void { + $this->container->set('config', $config); + + $factory = new LegacyXForwardedHeaderFilterFactory(); + $filter = $factory($this->container); + $request = $this->generateServerRequest($headers, $server, $baseUriString); + + $filteredRequest = $filter->filterRequest($request); + + if ($expectUnfiltered) { + $this->assertSame($request, $filteredRequest); + return; + } + + $this->assertNotSame($request, $filteredRequest); + $this->assertSame($expectedUriString, $filteredRequest->getUri()->__toString()); + } +} From 79bb88020c59cdefea951887151a218fbbf46e3a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Wed, 22 Jun 2022 17:25:27 -0500 Subject: [PATCH 12/45] feat: add NoOpRequestFilterFactory This patch adds the `NoOpRequestFilterFactory` to make registering the `NoOpRequestFilter` as a `ServerRequestFilterInterface` implemenation easier. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 20 +++++++++++++++++++ .../NoOpRequestFilterFactory.php | 13 ++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/ServerRequestFilter/NoOpRequestFilterFactory.php diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 6d8cbcd9..61adfcee 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -30,6 +30,26 @@ We provide the following implementations: - `NoOpRequestFilter`: returns the provided `$request` verbatim. - `LegacyXForwardedHeaderFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. +### NoOpRequestFilter + +This filter returns the `$request` argument back verbatim when invoked. + +#### NoOpRequestFilterFactory + +Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` via the `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilterFactory` class. +Register it as follows: + +```php +$config = [ + 'dependencies' => [ + 'factories' => [ + \Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface::class => + \Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilterFactory::class, + ], + ], +]; +``` + ### LegacyXForwardedHeaderFilter Servers behind a reverse proxy need mechanisms to determine the original URL requested. diff --git a/src/ServerRequestFilter/NoOpRequestFilterFactory.php b/src/ServerRequestFilter/NoOpRequestFilterFactory.php new file mode 100644 index 00000000..11355cca --- /dev/null +++ b/src/ServerRequestFilter/NoOpRequestFilterFactory.php @@ -0,0 +1,13 @@ + Date: Thu, 23 Jun 2022 08:40:26 -0500 Subject: [PATCH 13/45] refactor: rename LegacyXForwardedHeaderFilter* to XForwardedHeaderFilter* Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/api.md | 4 +- docs/book/v2/forward-migration.md | 7 +- docs/book/v2/server-request-filters.md | 30 +++--- src/ConfigProvider.php | 2 +- src/ServerRequestFactory.php | 8 +- ...rFilter.php => XForwardedHeaderFilter.php} | 4 +- ....php => XForwardedHeaderFilterFactory.php} | 10 +- ... => XForwardedHeaderFilterFactoryTest.php} | 94 +++++++++---------- ...est.php => XForwardedHeaderFilterTest.php} | 28 +++--- 9 files changed, 93 insertions(+), 94 deletions(-) rename src/ServerRequestFilter/{LegacyXForwardedHeaderFilter.php => XForwardedHeaderFilter.php} (97%) rename src/ServerRequestFilter/{LegacyXForwardedHeaderFilterFactory.php => XForwardedHeaderFilterFactory.php} (80%) rename test/ServerRequestFilter/{LegacyXForwardedHeaderFilterFactoryTest.php => XForwardedHeaderFilterFactoryTest.php} (76%) rename test/ServerRequestFilter/{LegacyXForwardedHeaderFilterTest.php => XForwardedHeaderFilterTest.php} (91%) diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index 0a1d603c..e2f2fe35 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -129,10 +129,10 @@ $request = ServerRequestFactory::fromGlobals( Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface`](server-request-filters.md). -For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter`](server-request-filters.md#legacyxforwardedheaderfilter) instance configured as follows: +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter`](server-request-filters.md#legacyxforwardedheaderfilter) instance configured as follows: ```php -$requestFilter = new LegacyXForwardedHeaderFilter(); +$requestFilter = new XForwardedHeaderFilter(); $requestFilter->trustAny(); ``` diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md index 80b98f9c..3baee05a 100644 --- a/docs/book/v2/forward-migration.md +++ b/docs/book/v2/forward-migration.md @@ -7,16 +7,15 @@ The primary use case is to allow modifying the generated URI based on the presen When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. (We have always examined the `X-Forwarded-Proto` header; as of 2.11.1, we also examine the `X-Forwarded-Port` header.) -To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter`. -(The "Legacy" verbiage is because a [new RFC (7239)](https://datatracker.ietf.org/doc/html/rfc7239) provides an official specification for this behavior via a new `Forwarded` header.) +To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter`. Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. (This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) -`LegacyXForwardedHeaderFilter` provides methods to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. +`XForwardedHeaderFilter` provides methods to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. To prevent backwards compatibility breaks, we use this filter by default, marked to trust any proxy. However, **in version 3, we will use a no-op filter by default**. -Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `LegacyXForwardedHeaderFilter` if you depend on this functionality. +Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `XForwardedHeaderFilter` if you depend on this functionality. If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` as the configured `ServerRequestFilterInterface` in your application immediately. We will update this documentation with a link to the related functionality in mezzio/mezzio when it is published. diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 61adfcee..325a193c 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -28,7 +28,7 @@ interface ServerRequestFilterInterface We provide the following implementations: - `NoOpRequestFilter`: returns the provided `$request` verbatim. -- `LegacyXForwardedHeaderFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. +- `XForwardedHeaderFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. ### NoOpRequestFilter @@ -50,7 +50,7 @@ $config = [ ]; ``` -### LegacyXForwardedHeaderFilter +### XForwardedHeaderFilter Servers behind a reverse proxy need mechanisms to determine the original URL requested. As such, reverse proxies have provided a number of mechanisms for delivering this information, with the use of `X-Forwarded-*` headers being the most prevalant. @@ -60,11 +60,11 @@ These include: - `X-Forwarded-Port`: the original port included in the `Host` header value. - `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). -`Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter` provides mechanisms for accepting these headers and using them to modify the URI composed in the request instance to match the original request. +`Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter` provides mechanisms for accepting these headers and using them to modify the URI composed in the request instance to match the original request. These methods are: - `trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. -- `trustProxies(string|string[] $proxies, string[] $trustedHeaders = LegacyXForwardedHeaderFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. +- `trustProxies(string|string[] $proxies, string[] $trustedHeaders = XForwardedHeaderFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. Order of operations matters when configuring the instance. If `trustAny()` is called after `trustProxies()`, the filter will trust any request. @@ -75,7 +75,7 @@ Internally, the filter checks the `REMOTE_ADDR` server parameter (as retrieved f #### Constants -The `LegacyXForwardedHeaderFilter` defines the following constants for use in specifying various headers: +The `XForwardedHeaderFilter` defines the following constants for use in specifying various headers: - `HEADER_HOST`: corresponds to `X-Forwarded-Host`. - `HEADER_PORT`: corresponds to `X-Forwarded-Port`. @@ -87,40 +87,40 @@ The `LegacyXForwardedHeaderFilter` defines the following constants for use in sp Trusting all `X-Forwarded-*` headers from any source: ```php -$filter = new LegacyXForwardedHeaderFilter(); +$filter = new XForwardedHeaderFilter(); $filter->trustAny(); ``` Trusting only the `X-Forwarded-Host` header from any source: ```php -$filter = new LegacyXForwardedHeaderFilter(); -$filter->trustProxies('0.0.0.0/0', [LegacyXForwardedHeaderFilter::HEADER_HOST]); +$filter = new XForwardedHeaderFilter(); +$filter->trustProxies('0.0.0.0/0', [XForwardedHeaderFilter::HEADER_HOST]); ``` Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a Class C subnet: ```php -$filter = new LegacyXForwardedHeaderFilter(); +$filter = new XForwardedHeaderFilter(); $filter->trustProxies( '192.168.1.0/24', - [LegacyXForwardedHeaderFilter::HEADER_HOST, LegacyXForwardedHeaderFilter::HEADER_PROTO] + [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] ); ``` Trusting the `X-Forwarded-Host` header from either a Class A or a Class C subnet: ```php -$filter = new LegacyXForwardedHeaderFilter(); +$filter = new XForwardedHeaderFilter(); $filter->trustProxies( ['10.1.1.0/16', '192.168.1.0/24'], - [LegacyXForwardedHeaderFilter::HEADER_HOST, LegacyXForwardedHeaderFilter::HEADER_PROTO] + [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] ); ``` -#### LegacyXForwardedHeaderFilterFactory +#### XForwardedHeaderFilterFactory -Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter` via the `Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilterFactory` class. +Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter` via the `Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilterFactory` class. This factory looks for the following configuration in order to generate an instance: ```php @@ -149,7 +149,7 @@ $config = [ 'dependencies' => [ 'factories' => [ \Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface::class => - \Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilterFactory::class, + \Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilterFactory::class, ], ], ]; diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 70d5fdc1..ee602ef6 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -45,7 +45,7 @@ public function getDependencies() : array ResponseFactoryInterface::class => ResponseFactory::class, StreamFactoryInterface::class => StreamFactory::class, ServerRequestFactoryInterface::class => ServerRequestFactory::class, - ServerRequestFilter\LegacyXForwardedHeaderFilter::class => ServerRequestFilter\LegacyXForwardedHeaderFilterFactory::class, + ServerRequestFilter\XForwardedHeaderFilter::class => ServerRequestFilter\XForwardedHeaderFilterFactory::class, UploadedFileFactoryInterface::class => UploadedFileFactory::class, UriFactoryInterface::class => UriFactory::class ], diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index e44c0ca3..d60323e1 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -4,7 +4,7 @@ namespace Laminas\Diactoros; -use Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter; use Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -47,8 +47,8 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * @param null|ServerRequestFilterInterface $requestFilter If present, the * generated request will be passed to this instance and the result * returned by this method. When not present, a default instance - * is created and used. For version 2, that instance is of - * LegacyXForwardedHeaderFilter, with the `trustAny()` method called. + * is created and used. For version 2, that instance is an + * XForwardedHeaderFilter, with the `trustAny()` method called. * For version 3, it will be a NoOpRequestFilter instance. * @return ServerRequest */ @@ -62,7 +62,7 @@ public static function fromGlobals( ) : ServerRequest { // @todo For version 3, we should instead create a NoOpRequestFilter instance. if (null === $requestFilter) { - $requestFilter = new LegacyXForwardedHeaderFilter(); + $requestFilter = new XForwardedHeaderFilter(); $requestFilter->trustAny(); } diff --git a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php b/src/ServerRequestFilter/XForwardedHeaderFilter.php similarity index 97% rename from src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php rename to src/ServerRequestFilter/XForwardedHeaderFilter.php index 2b7f9c73..257e1787 100644 --- a/src/ServerRequestFilter/LegacyXForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilter.php @@ -8,7 +8,7 @@ use Laminas\Diactoros\Exception\InvalidProxyAddressException; use Psr\Http\Message\ServerRequestInterface; -final class LegacyXForwardedHeaderFilter implements ServerRequestFilterInterface +final class XForwardedHeaderFilter implements ServerRequestFilterInterface { public const HEADER_HOST = 'X-FORWARDED-HOST'; public const HEADER_PORT = 'X-FORWARDED-PORT'; @@ -28,7 +28,7 @@ final class LegacyXForwardedHeaderFilter implements ServerRequestFilterInterface /** * @var string[] - * @psalm-var array + * @psalm-var array */ private $trustedHeaders = []; diff --git a/src/ServerRequestFilter/LegacyXForwardedHeaderFilterFactory.php b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php similarity index 80% rename from src/ServerRequestFilter/LegacyXForwardedHeaderFilterFactory.php rename to src/ServerRequestFilter/XForwardedHeaderFilterFactory.php index a6e07cbd..a2f0bc3e 100644 --- a/src/ServerRequestFilter/LegacyXForwardedHeaderFilterFactory.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php @@ -7,14 +7,14 @@ use Laminas\Diactoros\ConfigProvider; use Psr\Container\ContainerInterface; -final class LegacyXForwardedHeaderFilterFactory +final class XForwardedHeaderFilterFactory { - public function __invoke(ContainerInterface $container): LegacyXForwardedHeaderFilter + public function __invoke(ContainerInterface $container): XForwardedHeaderFilter { $config = $container->get('config'); $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::LEGACY_X_FORWARDED] ?? []; - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); if (empty($config)) { return $filter; @@ -40,7 +40,7 @@ public function __invoke(ContainerInterface $container): LegacyXForwardedHeaderF $headers = array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS, $config) ? $config[ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS] - : LegacyXForwardedHeaderFilter::X_FORWARDED_HEADERS; + : XForwardedHeaderFilter::X_FORWARDED_HEADERS; if (! is_array($headers)) { // Invalid value @@ -48,7 +48,7 @@ public function __invoke(ContainerInterface $container): LegacyXForwardedHeaderF } // Empty headers list implies trust all - $headers = empty($headers) ? LegacyXForwardedHeaderFilter::X_FORWARDED_HEADERS : $headers; + $headers = empty($headers) ? XForwardedHeaderFilter::X_FORWARDED_HEADERS : $headers; $filter->trustProxies($proxies, $headers); diff --git a/test/ServerRequestFilter/LegacyXForwardedHeaderFilterFactoryTest.php b/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php similarity index 76% rename from test/ServerRequestFilter/LegacyXForwardedHeaderFilterFactoryTest.php rename to test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php index a3f7117a..20f8471a 100644 --- a/test/ServerRequestFilter/LegacyXForwardedHeaderFilterFactoryTest.php +++ b/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php @@ -6,12 +6,12 @@ use Laminas\Diactoros\ConfigProvider; use Laminas\Diactoros\ServerRequest; -use Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter; -use Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilterFactory; +use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilterFactory; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; -class LegacyXForwardedHeaderFilterFactoryTest extends TestCase +class XForwardedHeaderFilterFactoryTest extends TestCase { /** @var ContainerInterface */ private $container; @@ -70,13 +70,13 @@ public function randomIpGenerator(): iterable /** @dataProvider randomIpGenerator */ public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(string $remoteAddr): void { - $factory = new LegacyXForwardedHeaderFilterFactory(); + $factory = new XForwardedHeaderFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -92,9 +92,9 @@ public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(str public function trustAnyProvider(): iterable { $headers = [ - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', - LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_PORT => '4443', ]; foreach ($this->randomIpGenerator() as $name => $arguments) { @@ -117,7 +117,7 @@ public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( ], ]); - $factory = new LegacyXForwardedHeaderFilterFactory(); + $factory = new XForwardedHeaderFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( $headers, @@ -129,10 +129,10 @@ public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( $this->assertNotSame($request, $filteredRequest); $uri = $filteredRequest->getUri(); - $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); + $this->assertSame($headers[XForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); // Port is always cast to int - $this->assertSame((int) $headers[LegacyXForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); - $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); + $this->assertSame((int) $headers[XForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); + $this->assertSame($headers[XForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); } /** @dataProvider trustAnyProvider */ @@ -152,7 +152,7 @@ public function testEnabledTrustAnyFlagHasPrecedenceOverTrustedProxiesConfig( ], ]); - $factory = new LegacyXForwardedHeaderFilterFactory(); + $factory = new XForwardedHeaderFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( $headers, @@ -164,10 +164,10 @@ public function testEnabledTrustAnyFlagHasPrecedenceOverTrustedProxiesConfig( $this->assertNotSame($request, $filteredRequest); $uri = $filteredRequest->getUri(); - $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); + $this->assertSame($headers[XForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); // Port is always cast to int - $this->assertSame((int) $headers[LegacyXForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); - $this->assertSame($headers[LegacyXForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); + $this->assertSame((int) $headers[XForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); + $this->assertSame($headers[XForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); } /** @dataProvider randomIpGenerator */ @@ -179,19 +179,19 @@ public function testEmptyProxiesListDoesNotTrustXForwardedHeaders(string $remote ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => [], ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ - LegacyXForwardedHeaderFilter::HEADER_HOST, + XForwardedHeaderFilter::HEADER_HOST, ], ], ], ]); - $factory = new LegacyXForwardedHeaderFilterFactory(); + $factory = new XForwardedHeaderFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -216,14 +216,14 @@ public function testEmptyHeadersListTrustsAllXForwardedHeadersForMatchedProxies( ], ]); - $factory = new LegacyXForwardedHeaderFilterFactory(); + $factory = new XForwardedHeaderFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', - LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_PORT => '4443', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -260,16 +260,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ - LegacyXForwardedHeaderFilter::HEADER_HOST, + XForwardedHeaderFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', - LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -284,16 +284,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ - LegacyXForwardedHeaderFilter::HEADER_HOST, + XForwardedHeaderFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', - LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -308,17 +308,17 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ - LegacyXForwardedHeaderFilter::HEADER_HOST, - LegacyXForwardedHeaderFilter::HEADER_PROTO, + XForwardedHeaderFilter::HEADER_HOST, + XForwardedHeaderFilter::HEADER_PROTO, ], ], ], ], [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', - LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -333,16 +333,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ - LegacyXForwardedHeaderFilter::HEADER_HOST, + XForwardedHeaderFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', - LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.2.1'], 'http://localhost/foo/bar', @@ -357,16 +357,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ - LegacyXForwardedHeaderFilter::HEADER_HOST, + XForwardedHeaderFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - LegacyXForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - LegacyXForwardedHeaderFilter::HEADER_PROTO => 'https', - LegacyXForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', + XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedHeaderFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.2.1'], 'http://localhost/foo/bar', @@ -385,7 +385,7 @@ public function testCombinedProxiesAndHeadersDefineTrust( ): void { $this->container->set('config', $config); - $factory = new LegacyXForwardedHeaderFilterFactory(); + $factory = new XForwardedHeaderFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest($headers, $server, $baseUriString); diff --git a/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php b/test/ServerRequestFilter/XForwardedHeaderFilterTest.php similarity index 91% rename from test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php rename to test/ServerRequestFilter/XForwardedHeaderFilterTest.php index 8812084d..32db1898 100644 --- a/test/ServerRequestFilter/LegacyXForwardedHeaderFilterTest.php +++ b/test/ServerRequestFilter/XForwardedHeaderFilterTest.php @@ -6,11 +6,11 @@ use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException; use Laminas\Diactoros\Exception\InvalidProxyAddressException; -use Laminas\Diactoros\ServerRequestFilter\LegacyXForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter; use Laminas\Diactoros\ServerRequest; use PHPUnit\Framework\TestCase; -class LegacyXForwardedHeaderFilterTest extends TestCase +class XForwardedHeaderFilterTest extends TestCase { public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllForwardedHeadersForThatProxy(): void { @@ -28,7 +28,7 @@ public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllF ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustProxies('192.168.1.0/24'); $filteredRequest = $filter->filterRequest($request); @@ -55,7 +55,7 @@ public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThose ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustProxies( '192.168.1.0/24', [$filter::HEADER_HOST, $filter::HEADER_PROTO] @@ -85,7 +85,7 @@ public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustProxies('192.168.1.0/24'); $filteredRequest = $filter->filterRequest($request); @@ -118,7 +118,7 @@ public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwa ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $filteredRequest = $filter->filterRequest($request); @@ -146,7 +146,7 @@ public function testTrustingProxyListWithSpecificTrustedHeadersTrustsOnlyThoseHe ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustProxies( ['192.168.1.0/24', '10.1.0.0/16'], [$filter::HEADER_HOST, $filter::HEADER_PROTO] @@ -184,7 +184,7 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $this->assertSame($request, $filter->filterRequest($request)); @@ -192,21 +192,21 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re public function testPassingInvalidStringAddressForProxyRaisesException(): void { - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $this->expectException(InvalidProxyAddressException::class); $filter->trustProxies('192.168.1'); } public function testPassingInvalidAddressInProxyListRaisesException(): void { - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $this->expectException(InvalidProxyAddressException::class); $filter->trustProxies(['192.168.1']); } public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void { - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $this->expectException(InvalidForwardedHeaderNameException::class); $filter->trustProxies('192.168.1.0/24', ['Host']); } @@ -225,7 +225,7 @@ public function testListOfForwardedHostsIsConsideredUntrusted(): void ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustAny(); $this->assertSame($request, $filter->filterRequest($request)); @@ -245,7 +245,7 @@ public function testListOfForwardedPortsIsConsideredUntrusted(): void ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustAny(); $this->assertSame($request, $filter->filterRequest($request)); @@ -265,7 +265,7 @@ public function testListOfForwardedProtosIsConsideredUntrusted(): void ] ); - $filter = new LegacyXForwardedHeaderFilter(); + $filter = new XForwardedHeaderFilter(); $filter->trustAny(); $this->assertSame($request, $filter->filterRequest($request)); From e5333bf3f8c65824beba54a22e509bde465c8333 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 08:53:55 -0500 Subject: [PATCH 14/45] refactor: make trustAny() and trustProxies() named constructors instead of instance methods This keeps the class immutable, and simplifies consumption. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/api.md | 3 +- docs/book/v2/forward-migration.md | 2 +- docs/book/v2/server-request-filters.md | 29 ++--- src/ServerRequestFactory.php | 7 +- .../XForwardedHeaderFilter.php | 100 +++++++++++------- .../XForwardedHeaderFilterFactory.php | 15 +-- .../XForwardedHeaderFilterTest.php | 40 +++---- 7 files changed, 96 insertions(+), 100 deletions(-) diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index e2f2fe35..dbfa9557 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -132,8 +132,7 @@ This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerReque For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter`](server-request-filters.md#legacyxforwardedheaderfilter) instance configured as follows: ```php -$requestFilter = new XForwardedHeaderFilter(); -$requestFilter->trustAny(); +$requestFilter = $requestFilter ?: XForwardedHeaderFilter::trustAny(); ``` The request filter is called on the generated server request instance, and its result is returned from `fromGlobals()`. diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md index 3baee05a..742f25a3 100644 --- a/docs/book/v2/forward-migration.md +++ b/docs/book/v2/forward-migration.md @@ -11,7 +11,7 @@ To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\ Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. (This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) -`XForwardedHeaderFilter` provides methods to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. +`XForwardedHeaderFilter` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. To prevent backwards compatibility breaks, we use this filter by default, marked to trust any proxy. However, **in version 3, we will use a no-op filter by default**. diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 325a193c..60f12f7b 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -60,15 +60,12 @@ These include: - `X-Forwarded-Port`: the original port included in the `Host` header value. - `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). -`Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter` provides mechanisms for accepting these headers and using them to modify the URI composed in the request instance to match the original request. -These methods are: +`Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter` provides named constructors for choosing whether to never trust proxies, always trust proxies, or choose wich proxies and/or headers to trust in order to modify the URI composed in the request instance to match the original request. +These named constructors are: -- `trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. -- `trustProxies(string|string[] $proxies, string[] $trustedHeaders = XForwardedHeaderFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. - -Order of operations matters when configuring the instance. -If `trustAny()` is called after `trustProxies()`, the filter will trust any request. -If `trustProxies()` is called after `trustAny()`, the filter will trust only the proxy/ies provided to `trustProxies()`. +- `XForwardedHeaderFilter::trustNone(): void`: when this method is called, the filter will not trust any proxies, and return the request back verbatim. +- `XForwardedHeaderFilter::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. +- `XForwardedHeaderFilterFactory::trustProxies(string|string[] $proxies, string[] $trustedHeaders = XForwardedHeaderFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. When providing one or more proxies to `trustProxies()`, the values may be exact IP addresses, or subnets specified by [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). Internally, the filter checks the `REMOTE_ADDR` server parameter (as retrieved from `getServerParams()`) and compares it against each proxy listed; the first to match indicates trust. @@ -80,29 +77,26 @@ The `XForwardedHeaderFilter` defines the following constants for use in specifyi - `HEADER_HOST`: corresponds to `X-Forwarded-Host`. - `HEADER_PORT`: corresponds to `X-Forwarded-Port`. - `HEADER_PROTO`: corresponds to `X-Forwarded-Proto`. -- `X_FORWARDED_HEADERS`: corresponds to an array consisting of all of the above costants. +- `X_FORWARDED_HEADERS`: corresponds to an array consisting of all of the above constants. #### Example usage Trusting all `X-Forwarded-*` headers from any source: ```php -$filter = new XForwardedHeaderFilter(); -$filter->trustAny(); +$filter = XForwardedHeaderFilter::trustAny(); ``` Trusting only the `X-Forwarded-Host` header from any source: ```php -$filter = new XForwardedHeaderFilter(); -$filter->trustProxies('0.0.0.0/0', [XForwardedHeaderFilter::HEADER_HOST]); +$filter = XForwardedHeaderFilter::trustProxies('0.0.0.0/0', [XForwardedHeaderFilter::HEADER_HOST]); ``` Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a Class C subnet: ```php -$filter = new XForwardedHeaderFilter(); -$filter->trustProxies( +$filter = XForwardedHeaderFilter::trustProxies( '192.168.1.0/24', [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] ); @@ -111,8 +105,7 @@ $filter->trustProxies( Trusting the `X-Forwarded-Host` header from either a Class A or a Class C subnet: ```php -$filter = new XForwardedHeaderFilter(); -$filter->trustProxies( +$filter = XForwardedHeaderFilter::trustProxies( ['10.1.1.0/16', '192.168.1.0/24'], [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] ); @@ -136,7 +129,7 @@ $config = [ ``` - The `trust-any` key should be a boolean. - By default, it is `false`; toggling it `true` will cause the `trustAny()` method to be called on the generated instance. + By default, it is `false`; toggling it `true` will use the `trustAny()` constructor to generate the instance. This flag overrides the `trusted-proxies` configuration. - The `trusted-proxies` array should be a string IP address or CIDR notation, or an array of such values, each indicating a trusted proxy server or subnet of such servers. - The `trusted-headers` array should consist of one or more of the `X-Forwarded-Host`, `X-Forwarded-Port`, or `X-Forwarded-Proto` header names; the values are case insensitive. diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index d60323e1..07a5ed1f 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -48,7 +48,7 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * generated request will be passed to this instance and the result * returned by this method. When not present, a default instance * is created and used. For version 2, that instance is an - * XForwardedHeaderFilter, with the `trustAny()` method called. + * XForwardedHeaderFilter, using the `trustAny()` constructor. * For version 3, it will be a NoOpRequestFilter instance. * @return ServerRequest */ @@ -61,10 +61,7 @@ public static function fromGlobals( ?ServerRequestFilterInterface $requestFilter = null ) : ServerRequest { // @todo For version 3, we should instead create a NoOpRequestFilter instance. - if (null === $requestFilter) { - $requestFilter = new XForwardedHeaderFilter(); - $requestFilter->trustAny(); - } + $requestFilter = $requestFilter ?: XForwardedHeaderFilter::trustAny(); $server = normalizeServer( $server ?: $_SERVER, diff --git a/src/ServerRequestFilter/XForwardedHeaderFilter.php b/src/ServerRequestFilter/XForwardedHeaderFilter.php index 257e1787..f077a1eb 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilter.php @@ -35,6 +35,56 @@ final class XForwardedHeaderFilter implements ServerRequestFilterInterface /** @var string[] */ private $trustedProxies = []; + /** + * Do not trust any proxies, nor any X-FORWARDED-* headers. + */ + public static function trustNone(): self + { + $filter = new self(); + $filter->trustAny = false; + + return $filter; + } + + /** + * Trust any X-FORWARDED-* headers from any address. + * + * 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 + { + $filter = new self(); + $filter->trustAny = true; + $filter->trustedHeaders = self::X_FORWARDED_HEADERS; + + return $filter; + } + + /** + * @param string|string[] $proxies + * @param array $trustedHeaders + * @throws InvalidProxyAddressException + * @throws InvalidForwardedHeaderNameException + */ + public static function trustProxies( + $proxies, + array $trustedHeaders = self::X_FORWARDED_HEADERS + ): self { + $proxies = self::normalizeProxiesList($proxies); + self::validateTrustedHeaders($trustedHeaders); + + $filter = new self(); + $filter->trustAny = false; + $filter->trustedProxies = $proxies; + $filter->trustedHeaders = $trustedHeaders; + + return $filter; + } + // public function filterRequest(array $headers, string $remoteAddress): array public function filterRequest(ServerRequestInterface $request): ServerRequestInterface { @@ -81,40 +131,7 @@ public function filterRequest(ServerRequestInterface $request): ServerRequestInt return $request; } - /** - * Trust any X-FORWARDED-* headers from any address. - * - * 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 function trustAny(): void - { - $this->trustAny = true; - $this->trustedHeaders = self::X_FORWARDED_HEADERS; - } - - /** - * @param string|string[] $proxies - * @param array $trustedHeaders - * @throws InvalidProxyAddressException - * @throws InvalidForwardedHeaderNameException - */ - public function trustProxies( - $proxies, - array $trustedHeaders = self::X_FORWARDED_HEADERS - ): void { - $proxies = $this->normalizeProxiesList($proxies); - $this->validateTrustedHeaders($trustedHeaders); - - $this->trustAny = false; - $this->trustedProxies = $proxies; - $this->trustedHeaders = $trustedHeaders; - } - - public function isFromTrustedProxy(string $remoteAddress): bool + private function isFromTrustedProxy(string $remoteAddress): bool { if ($this->trustAny) { return true; @@ -130,7 +147,7 @@ public function isFromTrustedProxy(string $remoteAddress): bool } /** @throws InvalidForwardedHeaderNameException */ - private function validateTrustedHeaders(array $headers): void + private static function validateTrustedHeaders(array $headers): void { foreach ($headers as $header) { if (! in_array($header, self::X_FORWARDED_HEADERS, true)) { @@ -140,7 +157,7 @@ private function validateTrustedHeaders(array $headers): void } /** @throws InvalidProxyAddressException */ - private function normalizeProxiesList($proxies): array + private static function normalizeProxiesList($proxies): array { if (! is_array($proxies) && ! is_string($proxies)) { throw InvalidProxyAddressException::forInvalidProxyArgument($proxies); @@ -149,7 +166,7 @@ private function normalizeProxiesList($proxies): array $proxies = is_array($proxies) ? $proxies : [$proxies]; foreach ($proxies as $proxy) { - if (! $this->validateProxyCIDR($proxy)) { + if (! self::validateProxyCIDR($proxy)) { throw InvalidProxyAddressException::forAddress($proxy); } } @@ -161,7 +178,7 @@ private function normalizeProxiesList($proxies): array * @param mixed $cidr * @throws InvalidCIDRException */ - private function validateProxyCIDR($cidr): bool + private static function validateProxyCIDR($cidr): bool { if (! is_string($cidr)) { return false; @@ -196,4 +213,11 @@ private function validateProxyCIDR($cidr): bool ) ); } + + /** + * Only allow construction via named constructors + */ + private function __construct() + { + } } diff --git a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php index a2f0bc3e..7268c683 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php @@ -14,17 +14,14 @@ public function __invoke(ContainerInterface $container): XForwardedHeaderFilter $config = $container->get('config'); $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::LEGACY_X_FORWARDED] ?? []; - $filter = new XForwardedHeaderFilter(); - if (empty($config)) { - return $filter; + return XForwardedHeaderFilter::trustNone(); } if (array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY, $config) && $config[ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY] ) { - $filter->trustAny(); - return $filter; + return XForwardedHeaderFilter::trustAny(); } $proxies = array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES, $config) @@ -35,7 +32,7 @@ public function __invoke(ContainerInterface $container): XForwardedHeaderFilter || empty($proxies) ) { // Makes no sense to set trusted headers if no proxies are trusted - return $filter; + return XForwardedHeaderFilter::trustNone(); } $headers = array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS, $config) @@ -44,14 +41,12 @@ public function __invoke(ContainerInterface $container): XForwardedHeaderFilter if (! is_array($headers)) { // Invalid value - return $filter; + return XForwardedHeaderFilter::trustNone(); } // Empty headers list implies trust all $headers = empty($headers) ? XForwardedHeaderFilter::X_FORWARDED_HEADERS : $headers; - $filter->trustProxies($proxies, $headers); - - return $filter; + return XForwardedHeaderFilter::trustProxies($proxies, $headers); } } diff --git a/test/ServerRequestFilter/XForwardedHeaderFilterTest.php b/test/ServerRequestFilter/XForwardedHeaderFilterTest.php index 32db1898..1b325f97 100644 --- a/test/ServerRequestFilter/XForwardedHeaderFilterTest.php +++ b/test/ServerRequestFilter/XForwardedHeaderFilterTest.php @@ -28,8 +28,7 @@ public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllF ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustProxies('192.168.1.0/24'); + $filter = XForwardedHeaderFilter::trustProxies('192.168.1.0/24'); $filteredRequest = $filter->filterRequest($request); $filteredUri = $filteredRequest->getUri(); @@ -55,10 +54,9 @@ public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThose ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustProxies( + $filter = XForwardedHeaderFilter::trustProxies( '192.168.1.0/24', - [$filter::HEADER_HOST, $filter::HEADER_PROTO] + [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] ); $filteredRequest = $filter->filterRequest($request); @@ -85,8 +83,7 @@ public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustProxies('192.168.1.0/24'); + $filter = XForwardedHeaderFilter::trustProxies('192.168.1.0/24'); $filteredRequest = $filter->filterRequest($request); $filteredUri = $filteredRequest->getUri(); @@ -118,8 +115,7 @@ public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwa ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + $filter = XForwardedHeaderFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $filteredRequest = $filter->filterRequest($request); $filteredUri = $filteredRequest->getUri(); @@ -146,10 +142,9 @@ public function testTrustingProxyListWithSpecificTrustedHeadersTrustsOnlyThoseHe ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustProxies( + $filter = XForwardedHeaderFilter::trustProxies( ['192.168.1.0/24', '10.1.0.0/16'], - [$filter::HEADER_HOST, $filter::HEADER_PROTO] + [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] ); $filteredRequest = $filter->filterRequest($request); @@ -184,31 +179,27 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + $filter = XForwardedHeaderFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $this->assertSame($request, $filter->filterRequest($request)); } public function testPassingInvalidStringAddressForProxyRaisesException(): void { - $filter = new XForwardedHeaderFilter(); $this->expectException(InvalidProxyAddressException::class); - $filter->trustProxies('192.168.1'); + XForwardedHeaderFilter::trustProxies('192.168.1'); } public function testPassingInvalidAddressInProxyListRaisesException(): void { - $filter = new XForwardedHeaderFilter(); $this->expectException(InvalidProxyAddressException::class); - $filter->trustProxies(['192.168.1']); + XForwardedHeaderFilter::trustProxies(['192.168.1']); } public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void { - $filter = new XForwardedHeaderFilter(); $this->expectException(InvalidForwardedHeaderNameException::class); - $filter->trustProxies('192.168.1.0/24', ['Host']); + XForwardedHeaderFilter::trustProxies('192.168.1.0/24', ['Host']); } public function testListOfForwardedHostsIsConsideredUntrusted(): void @@ -225,8 +216,7 @@ public function testListOfForwardedHostsIsConsideredUntrusted(): void ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustAny(); + $filter = XForwardedHeaderFilter::trustAny(); $this->assertSame($request, $filter->filterRequest($request)); } @@ -245,8 +235,7 @@ public function testListOfForwardedPortsIsConsideredUntrusted(): void ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustAny(); + $filter = XForwardedHeaderFilter::trustAny(); $this->assertSame($request, $filter->filterRequest($request)); } @@ -265,8 +254,7 @@ public function testListOfForwardedProtosIsConsideredUntrusted(): void ] ); - $filter = new XForwardedHeaderFilter(); - $filter->trustAny(); + $filter = XForwardedHeaderFilter::trustAny(); $this->assertSame($request, $filter->filterRequest($request)); } From d48a3ec8dc8afb65f6bb4c9f0cf1438f2cd6642a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 09:03:43 -0500 Subject: [PATCH 15/45] docs: fix typo Signed-off-by: Matthew Weier O'Phinney --- src/functions/marshal_uri_from_sapi_safely.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/marshal_uri_from_sapi_safely.php b/src/functions/marshal_uri_from_sapi_safely.php index 63a65458..3b284266 100644 --- a/src/functions/marshal_uri_from_sapi_safely.php +++ b/src/functions/marshal_uri_from_sapi_safely.php @@ -17,7 +17,7 @@ use function substr; /** - * Marshal a Uri instance based on the values presnt in the $_SERVER array and headers. + * Marshal a Uri instance based on the values present in the $_SERVER array and headers. * * @param array $server SAPI parameters * @param array $headers HTTP request headers From 717aa369ebb14766c9494dca35d3a3924b5ff124 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 09:08:09 -0500 Subject: [PATCH 16/45] fix: tighten checks in XForwardedHeaderFilterFactory for config type Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/XForwardedHeaderFilterFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php index 7268c683..12309c3e 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php @@ -14,7 +14,7 @@ public function __invoke(ContainerInterface $container): XForwardedHeaderFilter $config = $container->get('config'); $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::LEGACY_X_FORWARDED] ?? []; - if (empty($config)) { + if (! is_array($config) || empty($config)) { return XForwardedHeaderFilter::trustNone(); } From b7dcd15afe87210b626608d89c93f1e345ec25cf Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 10:08:30 -0500 Subject: [PATCH 17/45] Update docs/book/v2/server-request-filters.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frank Brückner Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 60f12f7b..81bf4846 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -1,6 +1,7 @@ # Server Request Filters -> - Since laminas/laminas-diactoros 2.11.1 +INFO: **New Feature** +Available since version 2.11.1 Server request filters allow you to modify the initial state of a generated `ServerRequest` instance as returned from `Laminas\Diactoros\ServerRequestFactory::fromGlobals()`. Common use cases include: From 44d9b980d1fb83a28aad0145feb41f1bcc24eaef Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 10:09:43 -0500 Subject: [PATCH 18/45] fix: remove obsolete in-progress comment Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/XForwardedHeaderFilter.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ServerRequestFilter/XForwardedHeaderFilter.php b/src/ServerRequestFilter/XForwardedHeaderFilter.php index f077a1eb..e68597d3 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilter.php @@ -85,7 +85,6 @@ public static function trustProxies( return $filter; } - // public function filterRequest(array $headers, string $remoteAddress): array public function filterRequest(ServerRequestInterface $request): ServerRequestInterface { $remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? ''; From 0dd10ac83d3c5f54e8869e41772b318a4a53439a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 10:11:42 -0500 Subject: [PATCH 19/45] fix: remove @throws annotation No exceptions thrown from function. Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/XForwardedHeaderFilter.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ServerRequestFilter/XForwardedHeaderFilter.php b/src/ServerRequestFilter/XForwardedHeaderFilter.php index e68597d3..8d678e07 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilter.php @@ -175,7 +175,6 @@ private static function normalizeProxiesList($proxies): array /** * @param mixed $cidr - * @throws InvalidCIDRException */ private static function validateProxyCIDR($cidr): bool { From a7f07c3654ece3e5548ba9f39cdadfa8cfd8e059 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 10:14:03 -0500 Subject: [PATCH 20/45] refactor: remove "LEGACY_" and "legacy-" prefixes from configuration Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 2 +- src/ConfigProvider.php | 16 ++++++++-------- .../XForwardedHeaderFilterFactory.php | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 81bf4846..215b0ba3 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -120,7 +120,7 @@ This factory looks for the following configuration in order to generate an insta ```php $config = [ 'laminas-diactoros' => [ - 'legacy-x-forwarded-header-filter' => [ + 'x-forwarded-header-filter' => [ 'trust-any' => bool, 'trusted-proxies' => string|string[], 'trusted-headers' => string[], diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index ee602ef6..79ca79eb 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -14,10 +14,10 @@ class ConfigProvider { public const CONFIG_KEY = 'laminas-diactoros'; - public const LEGACY_X_FORWARDED = 'laminas-x-forwarded-header-filter'; - public const LEGACY_X_FORWARDED_TRUST_ANY = 'trust-any'; - public const LEGACY_X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies'; - public const LEGACY_X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers'; + public const X_FORWARDED = 'x-forwarded-header-filter'; + public const X_FORWARDED_TRUST_ANY = 'trust-any'; + public const X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies'; + public const X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers'; /** * Retrieve configuration for laminas-diactoros. @@ -56,10 +56,10 @@ public function getDependencies() : array public function getComponentConfig(): array { return [ - self::LEGACY_X_FORWARDED => [ - self::LEGACY_X_FORWARDED_TRUST_ANY => false, - self::LEGACY_X_FORWARDED_TRUSTED_PROXIES => [], - self::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [], + self::X_FORWARDED => [ + self::X_FORWARDED_TRUST_ANY => false, + self::X_FORWARDED_TRUSTED_PROXIES => [], + self::X_FORWARDED_TRUSTED_HEADERS => [], ], ]; } diff --git a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php index 12309c3e..c9d8c084 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php @@ -12,20 +12,20 @@ final class XForwardedHeaderFilterFactory public function __invoke(ContainerInterface $container): XForwardedHeaderFilter { $config = $container->get('config'); - $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::LEGACY_X_FORWARDED] ?? []; + $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::X_FORWARDED] ?? []; if (! is_array($config) || empty($config)) { return XForwardedHeaderFilter::trustNone(); } - if (array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY, $config) - && $config[ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY] + if (array_key_exists(ConfigProvider::X_FORWARDED_TRUST_ANY, $config) + && $config[ConfigProvider::X_FORWARDED_TRUST_ANY] ) { return XForwardedHeaderFilter::trustAny(); } - $proxies = array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES, $config) - ? $config[ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES] + $proxies = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_PROXIES, $config) + ? $config[ConfigProvider::X_FORWARDED_TRUSTED_PROXIES] : []; if ((! is_string($proxies) && ! is_array($proxies)) @@ -35,8 +35,8 @@ public function __invoke(ContainerInterface $container): XForwardedHeaderFilter return XForwardedHeaderFilter::trustNone(); } - $headers = array_key_exists(ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS, $config) - ? $config[ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS] + $headers = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_HEADERS, $config) + ? $config[ConfigProvider::X_FORWARDED_TRUSTED_HEADERS] : XForwardedHeaderFilter::X_FORWARDED_HEADERS; if (! is_array($headers)) { From de799eac27b8418f1a78ebb1f9138a13d1b75799 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 10:15:33 -0500 Subject: [PATCH 21/45] fix: explicit check against "true" Prevents truthy values accidently creating an open proxy. Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/XForwardedHeaderFilterFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php index c9d8c084..5ab24faa 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php +++ b/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php @@ -19,7 +19,7 @@ public function __invoke(ContainerInterface $container): XForwardedHeaderFilter } if (array_key_exists(ConfigProvider::X_FORWARDED_TRUST_ANY, $config) - && $config[ConfigProvider::X_FORWARDED_TRUST_ANY] + && true === $config[ConfigProvider::X_FORWARDED_TRUST_ANY] ) { return XForwardedHeaderFilter::trustAny(); } From 6f41dd6fbcd1ad03dfbd66250b087eecb60706ae Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 10:17:59 -0500 Subject: [PATCH 22/45] fix: update tests to remove usage of `LEGACY_` verbiage when reference constants. Signed-off-by: Matthew Weier O'Phinney --- .../XForwardedHeaderFilterFactoryTest.php | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php b/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php index 20f8471a..8a160361 100644 --- a/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php +++ b/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php @@ -111,8 +111,8 @@ public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( $headers['Host'] = 'localhost'; $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => true, + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => true, ], ], ]); @@ -143,9 +143,9 @@ public function testEnabledTrustAnyFlagHasPrecedenceOverTrustedProxiesConfig( $headers['Host'] = 'localhost'; $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => true, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => [ + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => true, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => [ '192.168.0.0/24', ], ], @@ -175,10 +175,10 @@ public function testEmptyProxiesListDoesNotTrustXForwardedHeaders(string $remote { $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => [], - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => false, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => [], + ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedHeaderFilter::HEADER_HOST, ], ], @@ -208,10 +208,10 @@ public function testEmptyHeadersListTrustsAllXForwardedHeadersForMatchedProxies( { $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['0.0.0.0/0'], - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [], + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => false, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['0.0.0.0/0'], + ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [], ], ], ]); @@ -256,10 +256,10 @@ public function trustedProxiesAndHeaders(): iterable false, [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => false, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', + ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedHeaderFilter::HEADER_HOST, ], ], @@ -280,10 +280,10 @@ public function trustedProxiesAndHeaders(): iterable false, [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => false, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], + ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedHeaderFilter::HEADER_HOST, ], ], @@ -304,10 +304,10 @@ public function trustedProxiesAndHeaders(): iterable false, [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => false, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], + ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO, ], @@ -329,10 +329,10 @@ public function trustedProxiesAndHeaders(): iterable true, [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => false, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], + ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedHeaderFilter::HEADER_HOST, ], ], @@ -353,10 +353,10 @@ public function trustedProxiesAndHeaders(): iterable false, [ ConfigProvider::CONFIG_KEY => [ - ConfigProvider::LEGACY_X_FORWARDED => [ - ConfigProvider::LEGACY_X_FORWARDED_TRUST_ANY => false, - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], - ConfigProvider::LEGACY_X_FORWARDED_TRUSTED_HEADERS => [ + ConfigProvider::X_FORWARDED => [ + ConfigProvider::X_FORWARDED_TRUST_ANY => false, + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], + ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedHeaderFilter::HEADER_HOST, ], ], From 599e3433cecb750502738ebec30b3ccc4a0c1e3f Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 14:30:47 -0500 Subject: [PATCH 23/45] refactor: rename XForwardedHeaderFilter to XForwardedRequestFilter Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/api.md | 4 +- docs/book/v2/forward-migration.md | 6 +- docs/book/v2/server-request-filters.md | 32 +++--- src/ServerRequestFactory.php | 6 +- ...Filter.php => XForwardedRequestFilter.php} | 4 +- ...php => XForwardedRequestFilterFactory.php} | 18 ++-- ...=> XForwardedRequestFilterFactoryTest.php} | 98 +++++++++---------- ...st.php => XForwardedRequestFilterTest.php} | 34 +++---- 8 files changed, 101 insertions(+), 101 deletions(-) rename src/ServerRequestFilter/{XForwardedHeaderFilter.php => XForwardedRequestFilter.php} (97%) rename src/ServerRequestFilter/{XForwardedHeaderFilterFactory.php => XForwardedRequestFilterFactory.php} (70%) rename test/ServerRequestFilter/{XForwardedHeaderFilterFactoryTest.php => XForwardedRequestFilterFactoryTest.php} (76%) rename test/ServerRequestFilter/{XForwardedHeaderFilterTest.php => XForwardedRequestFilterTest.php} (87%) diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index dbfa9557..475bfacb 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -129,10 +129,10 @@ $request = ServerRequestFactory::fromGlobals( Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface`](server-request-filters.md). -For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter`](server-request-filters.md#legacyxforwardedheaderfilter) instance configured as follows: +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter`](server-request-filters.md#legacyxforwardedrequestfilter) instance configured as follows: ```php -$requestFilter = $requestFilter ?: XForwardedHeaderFilter::trustAny(); +$requestFilter = $requestFilter ?: XForwardedRequestFilter::trustAny(); ``` The request filter is called on the generated server request instance, and its result is returned from `fromGlobals()`. diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md index 742f25a3..10c68789 100644 --- a/docs/book/v2/forward-migration.md +++ b/docs/book/v2/forward-migration.md @@ -7,15 +7,15 @@ The primary use case is to allow modifying the generated URI based on the presen When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. (We have always examined the `X-Forwarded-Proto` header; as of 2.11.1, we also examine the `X-Forwarded-Port` header.) -To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter`. +To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter`. Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. (This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) -`XForwardedHeaderFilter` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. +`XForwardedRequestFilter` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. To prevent backwards compatibility breaks, we use this filter by default, marked to trust any proxy. However, **in version 3, we will use a no-op filter by default**. -Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `XForwardedHeaderFilter` if you depend on this functionality. +Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `XForwardedRequestFilter` if you depend on this functionality. If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` as the configured `ServerRequestFilterInterface` in your application immediately. We will update this documentation with a link to the related functionality in mezzio/mezzio when it is published. diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 215b0ba3..b43fecdd 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -29,7 +29,7 @@ interface ServerRequestFilterInterface We provide the following implementations: - `NoOpRequestFilter`: returns the provided `$request` verbatim. -- `XForwardedHeaderFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. +- `XForwardedRequestFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. ### NoOpRequestFilter @@ -51,7 +51,7 @@ $config = [ ]; ``` -### XForwardedHeaderFilter +### XForwardedRequestFilter Servers behind a reverse proxy need mechanisms to determine the original URL requested. As such, reverse proxies have provided a number of mechanisms for delivering this information, with the use of `X-Forwarded-*` headers being the most prevalant. @@ -61,19 +61,19 @@ These include: - `X-Forwarded-Port`: the original port included in the `Host` header value. - `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). -`Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter` provides named constructors for choosing whether to never trust proxies, always trust proxies, or choose wich proxies and/or headers to trust in order to modify the URI composed in the request instance to match the original request. +`Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter` provides named constructors for choosing whether to never trust proxies, always trust proxies, or choose wich proxies and/or headers to trust in order to modify the URI composed in the request instance to match the original request. These named constructors are: -- `XForwardedHeaderFilter::trustNone(): void`: when this method is called, the filter will not trust any proxies, and return the request back verbatim. -- `XForwardedHeaderFilter::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. -- `XForwardedHeaderFilterFactory::trustProxies(string|string[] $proxies, string[] $trustedHeaders = XForwardedHeaderFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. +- `XForwardedRequestFilter::trustNone(): void`: when this method is called, the filter will not trust any proxies, and return the request back verbatim. +- `XForwardedRequestFilter::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. +- `XForwardedRequestFilterFactory::trustProxies(string|string[] $proxies, string[] $trustedHeaders = XForwardedRequestFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. When providing one or more proxies to `trustProxies()`, the values may be exact IP addresses, or subnets specified by [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). Internally, the filter checks the `REMOTE_ADDR` server parameter (as retrieved from `getServerParams()`) and compares it against each proxy listed; the first to match indicates trust. #### Constants -The `XForwardedHeaderFilter` defines the following constants for use in specifying various headers: +The `XForwardedRequestFilter` defines the following constants for use in specifying various headers: - `HEADER_HOST`: corresponds to `X-Forwarded-Host`. - `HEADER_PORT`: corresponds to `X-Forwarded-Port`. @@ -85,36 +85,36 @@ The `XForwardedHeaderFilter` defines the following constants for use in specifyi Trusting all `X-Forwarded-*` headers from any source: ```php -$filter = XForwardedHeaderFilter::trustAny(); +$filter = XForwardedRequestFilter::trustAny(); ``` Trusting only the `X-Forwarded-Host` header from any source: ```php -$filter = XForwardedHeaderFilter::trustProxies('0.0.0.0/0', [XForwardedHeaderFilter::HEADER_HOST]); +$filter = XForwardedRequestFilter::trustProxies('0.0.0.0/0', [XForwardedRequestFilter::HEADER_HOST]); ``` Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a Class C subnet: ```php -$filter = XForwardedHeaderFilter::trustProxies( +$filter = XForwardedRequestFilter::trustProxies( '192.168.1.0/24', - [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] + [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] ); ``` Trusting the `X-Forwarded-Host` header from either a Class A or a Class C subnet: ```php -$filter = XForwardedHeaderFilter::trustProxies( +$filter = XForwardedRequestFilter::trustProxies( ['10.1.1.0/16', '192.168.1.0/24'], - [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] + [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] ); ``` -#### XForwardedHeaderFilterFactory +#### XForwardedRequestFilterFactory -Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter` via the `Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilterFactory` class. +Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter` via the `Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilterFactory` class. This factory looks for the following configuration in order to generate an instance: ```php @@ -143,7 +143,7 @@ $config = [ 'dependencies' => [ 'factories' => [ \Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface::class => - \Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilterFactory::class, + \Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilterFactory::class, ], ], ]; diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index 07a5ed1f..ebfb888d 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -4,7 +4,7 @@ namespace Laminas\Diactoros; -use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; use Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -48,7 +48,7 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * generated request will be passed to this instance and the result * returned by this method. When not present, a default instance * is created and used. For version 2, that instance is an - * XForwardedHeaderFilter, using the `trustAny()` constructor. + * XForwardedRequestFilter, using the `trustAny()` constructor. * For version 3, it will be a NoOpRequestFilter instance. * @return ServerRequest */ @@ -61,7 +61,7 @@ public static function fromGlobals( ?ServerRequestFilterInterface $requestFilter = null ) : ServerRequest { // @todo For version 3, we should instead create a NoOpRequestFilter instance. - $requestFilter = $requestFilter ?: XForwardedHeaderFilter::trustAny(); + $requestFilter = $requestFilter ?: XForwardedRequestFilter::trustAny(); $server = normalizeServer( $server ?: $_SERVER, diff --git a/src/ServerRequestFilter/XForwardedHeaderFilter.php b/src/ServerRequestFilter/XForwardedRequestFilter.php similarity index 97% rename from src/ServerRequestFilter/XForwardedHeaderFilter.php rename to src/ServerRequestFilter/XForwardedRequestFilter.php index 8d678e07..d2bf9a1b 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilter.php +++ b/src/ServerRequestFilter/XForwardedRequestFilter.php @@ -8,7 +8,7 @@ use Laminas\Diactoros\Exception\InvalidProxyAddressException; use Psr\Http\Message\ServerRequestInterface; -final class XForwardedHeaderFilter implements ServerRequestFilterInterface +final class XForwardedRequestFilter implements ServerRequestFilterInterface { public const HEADER_HOST = 'X-FORWARDED-HOST'; public const HEADER_PORT = 'X-FORWARDED-PORT'; @@ -28,7 +28,7 @@ final class XForwardedHeaderFilter implements ServerRequestFilterInterface /** * @var string[] - * @psalm-var array + * @psalm-var array */ private $trustedHeaders = []; diff --git a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php b/src/ServerRequestFilter/XForwardedRequestFilterFactory.php similarity index 70% rename from src/ServerRequestFilter/XForwardedHeaderFilterFactory.php rename to src/ServerRequestFilter/XForwardedRequestFilterFactory.php index 5ab24faa..2c8ee29c 100644 --- a/src/ServerRequestFilter/XForwardedHeaderFilterFactory.php +++ b/src/ServerRequestFilter/XForwardedRequestFilterFactory.php @@ -7,21 +7,21 @@ use Laminas\Diactoros\ConfigProvider; use Psr\Container\ContainerInterface; -final class XForwardedHeaderFilterFactory +final class XForwardedRequestFilterFactory { - public function __invoke(ContainerInterface $container): XForwardedHeaderFilter + public function __invoke(ContainerInterface $container): XForwardedRequestFilter { $config = $container->get('config'); $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::X_FORWARDED] ?? []; if (! is_array($config) || empty($config)) { - return XForwardedHeaderFilter::trustNone(); + return XForwardedRequestFilter::trustNone(); } if (array_key_exists(ConfigProvider::X_FORWARDED_TRUST_ANY, $config) && true === $config[ConfigProvider::X_FORWARDED_TRUST_ANY] ) { - return XForwardedHeaderFilter::trustAny(); + return XForwardedRequestFilter::trustAny(); } $proxies = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_PROXIES, $config) @@ -32,21 +32,21 @@ public function __invoke(ContainerInterface $container): XForwardedHeaderFilter || empty($proxies) ) { // Makes no sense to set trusted headers if no proxies are trusted - return XForwardedHeaderFilter::trustNone(); + return XForwardedRequestFilter::trustNone(); } $headers = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_HEADERS, $config) ? $config[ConfigProvider::X_FORWARDED_TRUSTED_HEADERS] - : XForwardedHeaderFilter::X_FORWARDED_HEADERS; + : XForwardedRequestFilter::X_FORWARDED_HEADERS; if (! is_array($headers)) { // Invalid value - return XForwardedHeaderFilter::trustNone(); + return XForwardedRequestFilter::trustNone(); } // Empty headers list implies trust all - $headers = empty($headers) ? XForwardedHeaderFilter::X_FORWARDED_HEADERS : $headers; + $headers = empty($headers) ? XForwardedRequestFilter::X_FORWARDED_HEADERS : $headers; - return XForwardedHeaderFilter::trustProxies($proxies, $headers); + return XForwardedRequestFilter::trustProxies($proxies, $headers); } } diff --git a/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php b/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php similarity index 76% rename from test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php rename to test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php index 8a160361..42dda77e 100644 --- a/test/ServerRequestFilter/XForwardedHeaderFilterFactoryTest.php +++ b/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php @@ -6,12 +6,12 @@ use Laminas\Diactoros\ConfigProvider; use Laminas\Diactoros\ServerRequest; -use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter; -use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilterFactory; +use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; +use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilterFactory; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; -class XForwardedHeaderFilterFactoryTest extends TestCase +class XForwardedRequestFilterFactoryTest extends TestCase { /** @var ContainerInterface */ private $container; @@ -70,13 +70,13 @@ public function randomIpGenerator(): iterable /** @dataProvider randomIpGenerator */ public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(string $remoteAddr): void { - $factory = new XForwardedHeaderFilterFactory(); + $factory = new XForwardedRequestFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -92,9 +92,9 @@ public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(str public function trustAnyProvider(): iterable { $headers = [ - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', - XForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_PORT => '4443', ]; foreach ($this->randomIpGenerator() as $name => $arguments) { @@ -117,7 +117,7 @@ public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( ], ]); - $factory = new XForwardedHeaderFilterFactory(); + $factory = new XForwardedRequestFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( $headers, @@ -129,10 +129,10 @@ public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( $this->assertNotSame($request, $filteredRequest); $uri = $filteredRequest->getUri(); - $this->assertSame($headers[XForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); + $this->assertSame($headers[XForwardedRequestFilter::HEADER_HOST], $uri->getHost()); // Port is always cast to int - $this->assertSame((int) $headers[XForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); - $this->assertSame($headers[XForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); + $this->assertSame((int) $headers[XForwardedRequestFilter::HEADER_PORT], $uri->getPort()); + $this->assertSame($headers[XForwardedRequestFilter::HEADER_PROTO], $uri->getScheme()); } /** @dataProvider trustAnyProvider */ @@ -152,7 +152,7 @@ public function testEnabledTrustAnyFlagHasPrecedenceOverTrustedProxiesConfig( ], ]); - $factory = new XForwardedHeaderFilterFactory(); + $factory = new XForwardedRequestFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( $headers, @@ -164,14 +164,14 @@ public function testEnabledTrustAnyFlagHasPrecedenceOverTrustedProxiesConfig( $this->assertNotSame($request, $filteredRequest); $uri = $filteredRequest->getUri(); - $this->assertSame($headers[XForwardedHeaderFilter::HEADER_HOST], $uri->getHost()); + $this->assertSame($headers[XForwardedRequestFilter::HEADER_HOST], $uri->getHost()); // Port is always cast to int - $this->assertSame((int) $headers[XForwardedHeaderFilter::HEADER_PORT], $uri->getPort()); - $this->assertSame($headers[XForwardedHeaderFilter::HEADER_PROTO], $uri->getScheme()); + $this->assertSame((int) $headers[XForwardedRequestFilter::HEADER_PORT], $uri->getPort()); + $this->assertSame($headers[XForwardedRequestFilter::HEADER_PROTO], $uri->getScheme()); } /** @dataProvider randomIpGenerator */ - public function testEmptyProxiesListDoesNotTrustXForwardedHeaders(string $remoteAddr): void + public function testEmptyProxiesListDoesNotTrustXForwardedRequests(string $remoteAddr): void { $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ @@ -179,19 +179,19 @@ public function testEmptyProxiesListDoesNotTrustXForwardedHeaders(string $remote ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => [], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedHeaderFilter::HEADER_HOST, + XForwardedRequestFilter::HEADER_HOST, ], ], ], ]); - $factory = new XForwardedHeaderFilterFactory(); + $factory = new XForwardedRequestFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -204,7 +204,7 @@ public function testEmptyProxiesListDoesNotTrustXForwardedHeaders(string $remote } /** @dataProvider randomIpGenerator */ - public function testEmptyHeadersListTrustsAllXForwardedHeadersForMatchedProxies(string $remoteAddr): void + public function testEmptyHeadersListTrustsAllXForwardedRequestsForMatchedProxies(string $remoteAddr): void { $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ @@ -216,14 +216,14 @@ public function testEmptyHeadersListTrustsAllXForwardedHeadersForMatchedProxies( ], ]); - $factory = new XForwardedHeaderFilterFactory(); + $factory = new XForwardedRequestFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', - XForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_PORT => '4443', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -260,16 +260,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedHeaderFilter::HEADER_HOST, + XForwardedRequestFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', - XForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -284,16 +284,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedHeaderFilter::HEADER_HOST, + XForwardedRequestFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', - XForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -308,17 +308,17 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedHeaderFilter::HEADER_HOST, - XForwardedHeaderFilter::HEADER_PROTO, + XForwardedRequestFilter::HEADER_HOST, + XForwardedRequestFilter::HEADER_PROTO, ], ], ], ], [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', - XForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -333,16 +333,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedHeaderFilter::HEADER_HOST, + XForwardedRequestFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', - XForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.2.1'], 'http://localhost/foo/bar', @@ -357,16 +357,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedHeaderFilter::HEADER_HOST, + XForwardedRequestFilter::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedHeaderFilter::HEADER_HOST => 'api.example.com', - XForwardedHeaderFilter::HEADER_PROTO => 'https', - XForwardedHeaderFilter::HEADER_PORT => '4443', + XForwardedRequestFilter::HEADER_HOST => 'api.example.com', + XForwardedRequestFilter::HEADER_PROTO => 'https', + XForwardedRequestFilter::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.2.1'], 'http://localhost/foo/bar', @@ -385,7 +385,7 @@ public function testCombinedProxiesAndHeadersDefineTrust( ): void { $this->container->set('config', $config); - $factory = new XForwardedHeaderFilterFactory(); + $factory = new XForwardedRequestFilterFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest($headers, $server, $baseUriString); diff --git a/test/ServerRequestFilter/XForwardedHeaderFilterTest.php b/test/ServerRequestFilter/XForwardedRequestFilterTest.php similarity index 87% rename from test/ServerRequestFilter/XForwardedHeaderFilterTest.php rename to test/ServerRequestFilter/XForwardedRequestFilterTest.php index 1b325f97..df4bb7b8 100644 --- a/test/ServerRequestFilter/XForwardedHeaderFilterTest.php +++ b/test/ServerRequestFilter/XForwardedRequestFilterTest.php @@ -6,11 +6,11 @@ use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException; use Laminas\Diactoros\Exception\InvalidProxyAddressException; -use Laminas\Diactoros\ServerRequestFilter\XForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; use Laminas\Diactoros\ServerRequest; use PHPUnit\Framework\TestCase; -class XForwardedHeaderFilterTest extends TestCase +class XForwardedRequestFilterTest extends TestCase { public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllForwardedHeadersForThatProxy(): void { @@ -28,7 +28,7 @@ public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllF ] ); - $filter = XForwardedHeaderFilter::trustProxies('192.168.1.0/24'); + $filter = XForwardedRequestFilter::trustProxies('192.168.1.0/24'); $filteredRequest = $filter->filterRequest($request); $filteredUri = $filteredRequest->getUri(); @@ -54,9 +54,9 @@ public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThose ] ); - $filter = XForwardedHeaderFilter::trustProxies( + $filter = XForwardedRequestFilter::trustProxies( '192.168.1.0/24', - [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] + [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] ); $filteredRequest = $filter->filterRequest($request); @@ -83,7 +83,7 @@ public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void ] ); - $filter = XForwardedHeaderFilter::trustProxies('192.168.1.0/24'); + $filter = XForwardedRequestFilter::trustProxies('192.168.1.0/24'); $filteredRequest = $filter->filterRequest($request); $filteredUri = $filteredRequest->getUri(); @@ -98,7 +98,7 @@ public function trustedProxyList(): iterable } /** @dataProvider trustedProxyList */ - public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwardedHeadersForTrustedProxies( + public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwardedRequestsForTrustedProxies( string $remoteAddr ): void { $request = new ServerRequest( @@ -115,7 +115,7 @@ public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwa ] ); - $filter = XForwardedHeaderFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + $filter = XForwardedRequestFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $filteredRequest = $filter->filterRequest($request); $filteredUri = $filteredRequest->getUri(); @@ -142,9 +142,9 @@ public function testTrustingProxyListWithSpecificTrustedHeadersTrustsOnlyThoseHe ] ); - $filter = XForwardedHeaderFilter::trustProxies( + $filter = XForwardedRequestFilter::trustProxies( ['192.168.1.0/24', '10.1.0.0/16'], - [XForwardedHeaderFilter::HEADER_HOST, XForwardedHeaderFilter::HEADER_PROTO] + [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] ); $filteredRequest = $filter->filterRequest($request); @@ -179,7 +179,7 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re ] ); - $filter = XForwardedHeaderFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + $filter = XForwardedRequestFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $this->assertSame($request, $filter->filterRequest($request)); } @@ -187,19 +187,19 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re public function testPassingInvalidStringAddressForProxyRaisesException(): void { $this->expectException(InvalidProxyAddressException::class); - XForwardedHeaderFilter::trustProxies('192.168.1'); + XForwardedRequestFilter::trustProxies('192.168.1'); } public function testPassingInvalidAddressInProxyListRaisesException(): void { $this->expectException(InvalidProxyAddressException::class); - XForwardedHeaderFilter::trustProxies(['192.168.1']); + XForwardedRequestFilter::trustProxies(['192.168.1']); } public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void { $this->expectException(InvalidForwardedHeaderNameException::class); - XForwardedHeaderFilter::trustProxies('192.168.1.0/24', ['Host']); + XForwardedRequestFilter::trustProxies('192.168.1.0/24', ['Host']); } public function testListOfForwardedHostsIsConsideredUntrusted(): void @@ -216,7 +216,7 @@ public function testListOfForwardedHostsIsConsideredUntrusted(): void ] ); - $filter = XForwardedHeaderFilter::trustAny(); + $filter = XForwardedRequestFilter::trustAny(); $this->assertSame($request, $filter->filterRequest($request)); } @@ -235,7 +235,7 @@ public function testListOfForwardedPortsIsConsideredUntrusted(): void ] ); - $filter = XForwardedHeaderFilter::trustAny(); + $filter = XForwardedRequestFilter::trustAny(); $this->assertSame($request, $filter->filterRequest($request)); } @@ -254,7 +254,7 @@ public function testListOfForwardedProtosIsConsideredUntrusted(): void ] ); - $filter = XForwardedHeaderFilter::trustAny(); + $filter = XForwardedRequestFilter::trustAny(); $this->assertSame($request, $filter->filterRequest($request)); } From 8d7e1bb7d3d9f6c41bcd8b7cbb2b166604c33ad5 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 17:18:54 -0500 Subject: [PATCH 24/45] fix: correct class name import in exception class Signed-off-by: Matthew Weier O'Phinney --- src/Exception/InvalidForwardedHeaderNameException.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Exception/InvalidForwardedHeaderNameException.php b/src/Exception/InvalidForwardedHeaderNameException.php index f2ee3aca..97083a47 100644 --- a/src/Exception/InvalidForwardedHeaderNameException.php +++ b/src/Exception/InvalidForwardedHeaderNameException.php @@ -4,7 +4,7 @@ namespace Laminas\Diactoros\Exception; -use Laminas\Diactoros\ForwardedHeaderFilter; +use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; class InvalidForwardedHeaderNameException extends RuntimeException implements ExceptionInterface { @@ -17,7 +17,7 @@ public static function forHeader($name): self return new self(sprintf( 'Invalid X-Forwarded-* header name "%s" provided to %s', $name, - ForwardedHeaderFilter::class, + XForwardedRequestFilter::class )); } } From 39fcbda63f65f99b263d8d7f78158ad928ce37e6 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 17:20:31 -0500 Subject: [PATCH 25/45] fix: correct class names in XForwardedRequestFilter factory mapping Also adds missing NoOpRequestFilter service. Signed-off-by: Matthew Weier O'Phinney --- src/ConfigProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 79ca79eb..61f68ff7 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -45,7 +45,8 @@ public function getDependencies() : array ResponseFactoryInterface::class => ResponseFactory::class, StreamFactoryInterface::class => StreamFactory::class, ServerRequestFactoryInterface::class => ServerRequestFactory::class, - ServerRequestFilter\XForwardedHeaderFilter::class => ServerRequestFilter\XForwardedHeaderFilterFactory::class, + ServerRequestFilter\NoOpRequestFilter::class => ServerRequestFilter\NoOpRequestFilterFactory::class, + ServerRequestFilter\XForwardedRequestFilter::class => ServerRequestFilter\XForwardedRequestFilterFactory::class, UploadedFileFactoryInterface::class => UploadedFileFactory::class, UriFactoryInterface::class => UriFactory::class ], From 9ecfacb78be6f704c35fd63d47f0faf9214aa8df Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 17:21:36 -0500 Subject: [PATCH 26/45] fix: update anchor location to match heading Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index 475bfacb..09e0ed2e 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -129,7 +129,7 @@ $request = ServerRequestFactory::fromGlobals( Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface`](server-request-filters.md). -For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter`](server-request-filters.md#legacyxforwardedrequestfilter) instance configured as follows: +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter`](server-request-filters.md#xforwardedrequestfilter) instance configured as follows: ```php $requestFilter = $requestFilter ?: XForwardedRequestFilter::trustAny(); From 1c766b5d378a8b1baf0078688f8858c80456fb3d Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 17:22:24 -0500 Subject: [PATCH 27/45] fix: fix configuration key used in config provider and docs example to match name change Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 2 +- src/ConfigProvider.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index b43fecdd..81a3e040 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -120,7 +120,7 @@ This factory looks for the following configuration in order to generate an insta ```php $config = [ 'laminas-diactoros' => [ - 'x-forwarded-header-filter' => [ + 'x-forwarded-request-filter' => [ 'trust-any' => bool, 'trusted-proxies' => string|string[], 'trusted-headers' => string[], diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 61f68ff7..6a0e935a 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -14,7 +14,7 @@ class ConfigProvider { public const CONFIG_KEY = 'laminas-diactoros'; - public const X_FORWARDED = 'x-forwarded-header-filter'; + public const X_FORWARDED = 'x-forwarded-request-filter'; public const X_FORWARDED_TRUST_ANY = 'trust-any'; public const X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies'; public const X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers'; From 4a05331f00287f869c990102aa828930fea20e73 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 23 Jun 2022 17:34:00 -0500 Subject: [PATCH 28/45] refactor: do not use a "trust-any" flag in configuration Instead, use a string wildcard ("*"). When present, this gets translated to `['0.0.0.0/0']`, which has the same effect. It also means that if headers are specified in configuration, only those headers will be trusted. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 9 ++-- src/ConfigProvider.php | 4 +- .../XForwardedRequestFilterFactory.php | 10 ++-- .../XForwardedRequestFilterFactoryTest.php | 46 +------------------ 4 files changed, 11 insertions(+), 58 deletions(-) diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 81a3e040..960e1110 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -121,7 +121,6 @@ This factory looks for the following configuration in order to generate an insta $config = [ 'laminas-diactoros' => [ 'x-forwarded-request-filter' => [ - 'trust-any' => bool, 'trusted-proxies' => string|string[], 'trusted-headers' => string[], ], @@ -129,10 +128,10 @@ $config = [ ]; ``` -- The `trust-any` key should be a boolean. - By default, it is `false`; toggling it `true` will use the `trustAny()` constructor to generate the instance. - This flag overrides the `trusted-proxies` configuration. -- The `trusted-proxies` array should be a string IP address or CIDR notation, or an array of such values, each indicating a trusted proxy server or subnet of such servers. +- The `trusted-proxies` value may be one of the following: + - The string "*". This indicates that all originating addresses are trusted. + - A string IP address or CIDR notation value indicating a trusted proxy server or subnet. + - An array of string IP addresses or CIDR notation values. - The `trusted-headers` array should consist of one or more of the `X-Forwarded-Host`, `X-Forwarded-Port`, or `X-Forwarded-Proto` header names; the values are case insensitive. When the configuration is omitted or the array is empty, the assumption is to honor all `X-Forwarded-*` headers for trusted proxies. diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 6a0e935a..b59911b2 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -15,7 +15,6 @@ class ConfigProvider { public const CONFIG_KEY = 'laminas-diactoros'; public const X_FORWARDED = 'x-forwarded-request-filter'; - public const X_FORWARDED_TRUST_ANY = 'trust-any'; public const X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies'; public const X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers'; @@ -58,8 +57,7 @@ public function getComponentConfig(): array { return [ self::X_FORWARDED => [ - self::X_FORWARDED_TRUST_ANY => false, - self::X_FORWARDED_TRUSTED_PROXIES => [], + self::X_FORWARDED_TRUSTED_PROXIES => '', self::X_FORWARDED_TRUSTED_HEADERS => [], ], ]; diff --git a/src/ServerRequestFilter/XForwardedRequestFilterFactory.php b/src/ServerRequestFilter/XForwardedRequestFilterFactory.php index 2c8ee29c..d6319ec5 100644 --- a/src/ServerRequestFilter/XForwardedRequestFilterFactory.php +++ b/src/ServerRequestFilter/XForwardedRequestFilterFactory.php @@ -18,16 +18,13 @@ public function __invoke(ContainerInterface $container): XForwardedRequestFilter return XForwardedRequestFilter::trustNone(); } - if (array_key_exists(ConfigProvider::X_FORWARDED_TRUST_ANY, $config) - && true === $config[ConfigProvider::X_FORWARDED_TRUST_ANY] - ) { - return XForwardedRequestFilter::trustAny(); - } - $proxies = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_PROXIES, $config) ? $config[ConfigProvider::X_FORWARDED_TRUSTED_PROXIES] : []; + // '*' means trust any source as a trusted proxy for purposes of this factory + $proxies = $proxies === '*' ? ['0.0.0.0/0'] : $proxies; + if ((! is_string($proxies) && ! is_array($proxies)) || empty($proxies) ) { @@ -35,6 +32,7 @@ public function __invoke(ContainerInterface $container): XForwardedRequestFilter return XForwardedRequestFilter::trustNone(); } + // Missing trusted headers setting means all headers are considered trusted $headers = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_HEADERS, $config) ? $config[ConfigProvider::X_FORWARDED_TRUSTED_HEADERS] : XForwardedRequestFilter::X_FORWARDED_HEADERS; diff --git a/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php b/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php index 42dda77e..3d262f87 100644 --- a/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php +++ b/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php @@ -104,7 +104,7 @@ public function trustAnyProvider(): iterable } /** @dataProvider trustAnyProvider */ - public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( + public function testIfWildcardProxyAddressSpecifiedReturnsFilterConfiguredToTrustAny( string $remoteAddr, array $headers ): void { @@ -112,42 +112,7 @@ public function testIfTrustAnyFlagIsEnabledReturnsFilterConfiguredToTrustAny( $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => true, - ], - ], - ]); - - $factory = new XForwardedRequestFilterFactory(); - $filter = $factory($this->container); - $request = $this->generateServerRequest( - $headers, - ['REMOTE_ADDR' => $remoteAddr], - 'http://localhost/foo/bar', - ); - - $filteredRequest = $filter->filterRequest($request); - $this->assertNotSame($request, $filteredRequest); - - $uri = $filteredRequest->getUri(); - $this->assertSame($headers[XForwardedRequestFilter::HEADER_HOST], $uri->getHost()); - // Port is always cast to int - $this->assertSame((int) $headers[XForwardedRequestFilter::HEADER_PORT], $uri->getPort()); - $this->assertSame($headers[XForwardedRequestFilter::HEADER_PROTO], $uri->getScheme()); - } - - /** @dataProvider trustAnyProvider */ - public function testEnabledTrustAnyFlagHasPrecedenceOverTrustedProxiesConfig( - string $remoteAddr, - array $headers - ): void { - $headers['Host'] = 'localhost'; - $this->container->set('config', [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => true, - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => [ - '192.168.0.0/24', - ], + ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => '*', ], ], ]); @@ -176,7 +141,6 @@ public function testEmptyProxiesListDoesNotTrustXForwardedRequests(string $remot $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => [], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedRequestFilter::HEADER_HOST, @@ -209,7 +173,6 @@ public function testEmptyHeadersListTrustsAllXForwardedRequestsForMatchedProxies $this->container->set('config', [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['0.0.0.0/0'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [], ], @@ -257,7 +220,6 @@ public function trustedProxiesAndHeaders(): iterable [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedRequestFilter::HEADER_HOST, @@ -281,7 +243,6 @@ public function trustedProxiesAndHeaders(): iterable [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedRequestFilter::HEADER_HOST, @@ -305,7 +266,6 @@ public function trustedProxiesAndHeaders(): iterable [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedRequestFilter::HEADER_HOST, @@ -330,7 +290,6 @@ public function trustedProxiesAndHeaders(): iterable [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedRequestFilter::HEADER_HOST, @@ -354,7 +313,6 @@ public function trustedProxiesAndHeaders(): iterable [ ConfigProvider::CONFIG_KEY => [ ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUST_ANY => false, ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ XForwardedRequestFilter::HEADER_HOST, From 1e5b05e99d8a1e7e84d2f2c9b50db75615a6df57 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Fri, 24 Jun 2022 09:32:51 -0500 Subject: [PATCH 29/45] refactor: remove marshalUriFromSapiSafely() The functionality is now implemented as private static methods on the `ServerRequestFactory` class. All inlined closures were rewritten as discrete private static methods (as none actually closed over any values), and doing so outlined one or more that were no longer used. Additionally, the method was rewritten without the `$headers` argument, as it is no longer used internally. Signed-off-by: Matthew Weier O'Phinney --- composer.json | 1 - docs/book/v2/api.md | 8 +- src/ServerRequestFactory.php | 156 ++++++++++++++- src/functions/marshal_uri_from_sapi.php | 5 +- .../marshal_uri_from_sapi_safely.php | 182 ------------------ 5 files changed, 158 insertions(+), 194 deletions(-) delete mode 100644 src/functions/marshal_uri_from_sapi_safely.php diff --git a/composer.json b/composer.json index a9e6fc2b..27dffe53 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,6 @@ "src/functions/marshal_method_from_sapi.php", "src/functions/marshal_protocol_version_from_sapi.php", "src/functions/marshal_uri_from_sapi.php", - "src/functions/marshal_uri_from_sapi_safely.php", "src/functions/normalize_server.php", "src/functions/normalize_uploaded_files.php", "src/functions/parse_cookie_header.php", diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index 09e0ed2e..ea0e2911 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -153,12 +153,8 @@ and even the `Cookie` header. These include: - `Laminas\Diactoros\marshalProtocolVersionFromSapi(array $server) : string` - `Laminas\Diactoros\marshalMethodFromSapi(array $server) : string`. - `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers) : Uri`. - Please note: **this function is deprecated as of version 2.11.1**. - Use `Laminas\Diactoros\marshalUriFromSapiSafely()` instead. - This function is no longer used in `ServerRequestFactory::fromGlobals()`. -- `Laminas\Diactoros\marshalUriFromSapiSafely(array $server, array $headers) : Uri`. - This function differs from `Laminas\Diactoros\marshalUriFromSapi(array $server, array $headers)` in that it never considers `X-Forwarded-*` headers when generating the `Uri` instance composed in the generated `ServerRequest`. - It is the implementation used since version 2.11.1. + Please note: **this function is deprecated as of version 2.11.1**, and no longer used in `ServerRequestFactory::fromGlobals()`. + Use `ServerRequestFactory::fromGlobals()` instead. - `Laminas\Diactoros\marshalHeadersFromSapi(array $server) : array` - `Laminas\Diactoros\parseCookieHeader(string $header) : array` - `Laminas\Diactoros\createUploadedFile(array $spec) : UploadedFile` (creates the diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index ebfb888d..d522c3e9 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -77,7 +77,7 @@ public static function fromGlobals( return $requestFilter->filterRequest(new ServerRequest( $server, $files, - marshalUriFromSapiSafely($server, $headers), + self::marshalUriFromSapi($server), marshalMethodFromSapi($server), 'php://input', $headers, @@ -103,4 +103,158 @@ public function createServerRequest(string $method, $uri, array $serverParams = 'php://temp' ); } + + /** + * Marshal a Uri instance based on the values present in the $_SERVER array and headers. + * + * @param array $server SAPI parameters + */ + private static function marshalUriFromSapi(array $server) : Uri + { + $uri = new Uri(''); + + // URI scheme + $scheme = 'http'; + if (array_key_exists('HTTPS', $server)) { + $https = self::marshalHttpsValue($server['HTTPS']); + } elseif (array_key_exists('https', $server)) { + $https = self::marshalHttpsValue($server['https']); + } else { + $https = false; + } + + $scheme = $https ? 'https' : $scheme; + $uri = $uri->withScheme($scheme); + + // Set the host + [$host, $port] = self::marshalHostAndPort($server); + if (! empty($host)) { + $uri = $uri->withHost($host); + if (! empty($port)) { + $uri = $uri->withPort($port); + } + } + + // URI path + $path = self::marshalRequestPath($server); + + // Strip query string + $path = explode('?', $path, 2)[0]; + + // URI query + $query = ''; + if (isset($server['QUERY_STRING'])) { + $query = ltrim($server['QUERY_STRING'], '?'); + } + + // URI fragment + $fragment = ''; + if (strpos($path, '#') !== false) { + [$path, $fragment] = explode('#', $path, 2); + } + + return $uri + ->withPath($path) + ->withFragment($fragment) + ->withQuery($query); + } + + /** + * Marshal the host and port from the PHP environment. + * + * @return array Array of two items, host and port, in that order (can be + * passed to a list() operation). + */ + private static function marshalHostAndPort(array $server) : array + { + static $defaults = ['', null]; + + if (! isset($server['SERVER_NAME'])) { + return $defaults; + } + + $host = $server['SERVER_NAME']; + $port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null; + + if (! isset($server['SERVER_ADDR']) + || ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $host) + ) { + return [$host, $port]; + } + + // Misinterpreted IPv6-Address + // Reported for Safari on Windows + return self::marshalIpv6HostAndPort($server, $port); + } + + /** + * @return array Array of two items, host and port, in that order (can be + * passed to a list() operation). + */ + private static function marshalIpv6HostAndPort(array $server, ?int $port) : array + { + $host = '[' . $server['SERVER_ADDR'] . ']'; + $port = $port ?: 80; + if ($port . ']' === substr($host, strrpos($host, ':') + 1)) { + // The last digit of the IPv6-Address has been taken as port + // Unset the port so the default port can be used + $port = null; + } + return [$host, $port]; + } + + /** + * Detect the path for the request + * + * Looks at a variety of criteria in order to attempt to autodetect the base + * request path, including: + * + * - IIS7 UrlRewrite environment + * - REQUEST_URI + * - ORIG_PATH_INFO + * + * From Laminas\Http\PhpEnvironment\Request class + */ + private static function marshalRequestPath(array $server) : string + { + // IIS7 with URL Rewrite: make sure we get the unencoded url + // (double slash problem). + $iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null; + $unencodedUrl = $server['UNENCODED_URL'] ?? ''; + if ('1' === $iisUrlRewritten && ! empty($unencodedUrl)) { + return $unencodedUrl; + } + + $requestUri = $server['REQUEST_URI'] ?? null; + + if ($requestUri !== null) { + return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri); + } + + $origPathInfo = $server['ORIG_PATH_INFO'] ?? null; + if (empty($origPathInfo)) { + return '/'; + } + + return $origPathInfo; + } + + /** + * @param mixed $https + */ + private static function marshalHttpsValue($https) : bool + { + if (is_bool($https)) { + return $https; + } + + if (! is_string($https)) { + throw new Exception\InvalidArgumentException(sprintf( + 'SAPI HTTPS value MUST be a string or boolean; received %s', + gettype($https) + )); + } + + return 'on' === strtolower($https); + } } diff --git a/src/functions/marshal_uri_from_sapi.php b/src/functions/marshal_uri_from_sapi.php index 0268ce1f..0092aeff 100644 --- a/src/functions/marshal_uri_from_sapi.php +++ b/src/functions/marshal_uri_from_sapi.php @@ -23,10 +23,7 @@ * @param array $server SAPI parameters * @param array $headers HTTP request headers * @deprecated This function is deprecated as of 2.11.1, and will be removed in - * 3.0.0. For security purposes, we recommend using Laminas\Diactoros\marshalUriFromSapiSafely, - * and a Laminas\Diactoros\RequestFilter\RequestFilterInterface - * implmentation if you need the ability to use X-Forwarded-* headers to - * modify the generated URI. + * 3.0.0. As of 2.11.1, it is no longer used internally. */ function marshalUriFromSapi(array $server, array $headers) : Uri { diff --git a/src/functions/marshal_uri_from_sapi_safely.php b/src/functions/marshal_uri_from_sapi_safely.php deleted file mode 100644 index 3b284266..00000000 --- a/src/functions/marshal_uri_from_sapi_safely.php +++ /dev/null @@ -1,182 +0,0 @@ -withScheme($scheme); - - // Set the host - [$host, $port] = $marshalHostAndPort($server); - if (! empty($host)) { - $uri = $uri->withHost($host); - if (! empty($port)) { - $uri = $uri->withPort($port); - } - } - - // URI path - $path = $marshalRequestPath($server); - - // Strip query string - $path = explode('?', $path, 2)[0]; - - // URI query - $query = ''; - if (isset($server['QUERY_STRING'])) { - $query = ltrim($server['QUERY_STRING'], '?'); - } - - // URI fragment - $fragment = ''; - if (strpos($path, '#') !== false) { - [$path, $fragment] = explode('#', $path, 2); - } - - return $uri - ->withPath($path) - ->withFragment($fragment) - ->withQuery($query); -} From 561a8a23443d532c9618826d5f14bf527bcf86bc Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 11:30:11 -0500 Subject: [PATCH 30/45] refactor: rename filter interface Renames the filter interface to be a verb: `FilterServerRequestInterface`. Additionally, it renames the `filterRequest()` method to `__invoke()`. As such, all cases in tests and code where we were typehinting against this class and/or consuming it required updates. Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFactory.php | 8 ++--- .../FilterServerRequestInterface.php | 29 +++++++++++++++++++ src/ServerRequestFilter/NoOpRequestFilter.php | 4 +-- .../ServerRequestFilterInterface.php | 12 -------- .../XForwardedRequestFilter.php | 4 +-- test/ServerRequestFactoryTest.php | 6 ++-- .../NoOpRequestFilterTest.php | 2 +- .../XForwardedRequestFilterFactoryTest.php | 10 +++---- .../XForwardedRequestFilterTest.php | 18 ++++++------ 9 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 src/ServerRequestFilter/FilterServerRequestInterface.php delete mode 100644 src/ServerRequestFilter/ServerRequestFilterInterface.php diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index d522c3e9..8eea80ce 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -5,7 +5,7 @@ namespace Laminas\Diactoros; use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; -use Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface; +use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -44,7 +44,7 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * @param array $body $_POST superglobal * @param array $cookies $_COOKIE superglobal * @param array $files $_FILES superglobal - * @param null|ServerRequestFilterInterface $requestFilter If present, the + * @param null|FilterServerRequestInterface $requestFilter If present, the * generated request will be passed to this instance and the result * returned by this method. When not present, a default instance * is created and used. For version 2, that instance is an @@ -58,7 +58,7 @@ public static function fromGlobals( array $body = null, array $cookies = null, array $files = null, - ?ServerRequestFilterInterface $requestFilter = null + ?FilterServerRequestInterface $requestFilter = null ) : ServerRequest { // @todo For version 3, we should instead create a NoOpRequestFilter instance. $requestFilter = $requestFilter ?: XForwardedRequestFilter::trustAny(); @@ -74,7 +74,7 @@ public static function fromGlobals( $cookies = parseCookieHeader($headers['cookie']); } - return $requestFilter->filterRequest(new ServerRequest( + return $requestFilter(new ServerRequest( $server, $files, self::marshalUriFromSapi($server), diff --git a/src/ServerRequestFilter/FilterServerRequestInterface.php b/src/ServerRequestFilter/FilterServerRequestInterface.php new file mode 100644 index 00000000..4de57d6d --- /dev/null +++ b/src/ServerRequestFilter/FilterServerRequestInterface.php @@ -0,0 +1,29 @@ +getServerParams()['REMOTE_ADDR'] ?? ''; diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index fca18d21..fe3beb7a 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -6,7 +6,7 @@ use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory; -use Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface; +use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface; use Laminas\Diactoros\UploadedFile; use Laminas\Diactoros\Uri; use PHPUnit\Framework\TestCase; @@ -726,7 +726,7 @@ public function testDoesNotMarshalAllContentPrefixedServerVarsAsHeaders( public function testReturnsFilteredRequestBasedOnRequestFilterProvided(): void { $expectedRequest = new ServerRequest(); - $filter = new class($expectedRequest) implements ServerRequestFilterInterface { + $filter = new class($expectedRequest) implements FilterServerRequestInterface { /** @var ServerRequestInterface */ private $request; @@ -735,7 +735,7 @@ public function __construct(ServerRequestInterface $request) $this->request = $request; } - public function filterRequest(ServerRequestInterface $request): ServerRequestInterface + public function __invoke(ServerRequestInterface $request): ServerRequestInterface { return $this->request; } diff --git a/test/ServerRequestFilter/NoOpRequestFilterTest.php b/test/ServerRequestFilter/NoOpRequestFilterTest.php index f6cf884d..162f929b 100644 --- a/test/ServerRequestFilter/NoOpRequestFilterTest.php +++ b/test/ServerRequestFilter/NoOpRequestFilterTest.php @@ -15,6 +15,6 @@ public function testReturnsSameInstanceItWasProvided(): void $request = new ServerRequest(); $filter = new NoOpRequestFilter(); - $this->assertSame($request, $filter->filterRequest($request)); + $this->assertSame($request, $filter($request)); } } diff --git a/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php b/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php index 3d262f87..b4166f12 100644 --- a/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php +++ b/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php @@ -84,7 +84,7 @@ public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(str 'http://localhost/foo/bar', ); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $this->assertSame($request, $filteredRequest); } @@ -125,7 +125,7 @@ public function testIfWildcardProxyAddressSpecifiedReturnsFilterConfiguredToTrus 'http://localhost/foo/bar', ); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $this->assertNotSame($request, $filteredRequest); $uri = $filteredRequest->getUri(); @@ -163,7 +163,7 @@ public function testEmptyProxiesListDoesNotTrustXForwardedRequests(string $remot 'http://localhost/foo/bar', ); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $this->assertSame($request, $filteredRequest); } @@ -194,7 +194,7 @@ public function testEmptyHeadersListTrustsAllXForwardedRequestsForMatchedProxies 'http://localhost/foo/bar', ); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $this->assertNotSame($request, $filteredRequest); $uri = $filteredRequest->getUri(); @@ -347,7 +347,7 @@ public function testCombinedProxiesAndHeadersDefineTrust( $filter = $factory($this->container); $request = $this->generateServerRequest($headers, $server, $baseUriString); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); if ($expectUnfiltered) { $this->assertSame($request, $filteredRequest); diff --git a/test/ServerRequestFilter/XForwardedRequestFilterTest.php b/test/ServerRequestFilter/XForwardedRequestFilterTest.php index df4bb7b8..184279ee 100644 --- a/test/ServerRequestFilter/XForwardedRequestFilterTest.php +++ b/test/ServerRequestFilter/XForwardedRequestFilterTest.php @@ -30,7 +30,7 @@ public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllF $filter = XForwardedRequestFilter::trustProxies('192.168.1.0/24'); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); $this->assertNotSame($request->getUri(), $filteredUri); $this->assertSame('example.com', $filteredUri->getHost()); @@ -59,7 +59,7 @@ public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThose [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] ); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); $this->assertNotSame($request->getUri(), $filteredUri); $this->assertSame('example.com', $filteredUri->getHost()); @@ -85,7 +85,7 @@ public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void $filter = XForwardedRequestFilter::trustProxies('192.168.1.0/24'); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); $this->assertSame($request->getUri(), $filteredUri); } @@ -117,7 +117,7 @@ public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwa $filter = XForwardedRequestFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); $this->assertNotSame($request->getUri(), $filteredUri); $this->assertSame('example.com', $filteredUri->getHost()); @@ -147,7 +147,7 @@ public function testTrustingProxyListWithSpecificTrustedHeadersTrustsOnlyThoseHe [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] ); - $filteredRequest = $filter->filterRequest($request); + $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); $this->assertNotSame($request->getUri(), $filteredUri); $this->assertSame('example.com', $filteredUri->getHost()); @@ -181,7 +181,7 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re $filter = XForwardedRequestFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); - $this->assertSame($request, $filter->filterRequest($request)); + $this->assertSame($request, $filter($request)); } public function testPassingInvalidStringAddressForProxyRaisesException(): void @@ -218,7 +218,7 @@ public function testListOfForwardedHostsIsConsideredUntrusted(): void $filter = XForwardedRequestFilter::trustAny(); - $this->assertSame($request, $filter->filterRequest($request)); + $this->assertSame($request, $filter($request)); } public function testListOfForwardedPortsIsConsideredUntrusted(): void @@ -237,7 +237,7 @@ public function testListOfForwardedPortsIsConsideredUntrusted(): void $filter = XForwardedRequestFilter::trustAny(); - $this->assertSame($request, $filter->filterRequest($request)); + $this->assertSame($request, $filter($request)); } public function testListOfForwardedProtosIsConsideredUntrusted(): void @@ -256,6 +256,6 @@ public function testListOfForwardedProtosIsConsideredUntrusted(): void $filter = XForwardedRequestFilter::trustAny(); - $this->assertSame($request, $filter->filterRequest($request)); + $this->assertSame($request, $filter($request)); } } From 7780494ba7898288ba397f47ae96f3cfcf4437fb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 11:34:18 -0500 Subject: [PATCH 31/45] refactor: rename NoOpRequestFilter to DoNotFilter Per review. Signed-off-by: Matthew Weier O'Phinney --- src/ConfigProvider.php | 1 - src/ServerRequestFactory.php | 4 ++-- .../{NoOpRequestFilter.php => DoNotFilter.php} | 2 +- .../NoOpRequestFilterFactory.php | 13 ------------- ...oOpRequestFilterTest.php => DoNotFilterTest.php} | 6 +++--- 5 files changed, 6 insertions(+), 20 deletions(-) rename src/ServerRequestFilter/{NoOpRequestFilter.php => DoNotFilter.php} (78%) delete mode 100644 src/ServerRequestFilter/NoOpRequestFilterFactory.php rename test/ServerRequestFilter/{NoOpRequestFilterTest.php => DoNotFilterTest.php} (69%) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index b59911b2..baae3b46 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -44,7 +44,6 @@ public function getDependencies() : array ResponseFactoryInterface::class => ResponseFactory::class, StreamFactoryInterface::class => StreamFactory::class, ServerRequestFactoryInterface::class => ServerRequestFactory::class, - ServerRequestFilter\NoOpRequestFilter::class => ServerRequestFilter\NoOpRequestFilterFactory::class, ServerRequestFilter\XForwardedRequestFilter::class => ServerRequestFilter\XForwardedRequestFilterFactory::class, UploadedFileFactoryInterface::class => UploadedFileFactory::class, UriFactoryInterface::class => UriFactory::class diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index 8eea80ce..ef61b590 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -49,7 +49,7 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * returned by this method. When not present, a default instance * is created and used. For version 2, that instance is an * XForwardedRequestFilter, using the `trustAny()` constructor. - * For version 3, it will be a NoOpRequestFilter instance. + * For version 3, it will be a DoNotFilter instance. * @return ServerRequest */ public static function fromGlobals( @@ -60,7 +60,7 @@ public static function fromGlobals( array $files = null, ?FilterServerRequestInterface $requestFilter = null ) : ServerRequest { - // @todo For version 3, we should instead create a NoOpRequestFilter instance. + // @todo For version 3, we should instead create a DoNotFilter instance. $requestFilter = $requestFilter ?: XForwardedRequestFilter::trustAny(); $server = normalizeServer( diff --git a/src/ServerRequestFilter/NoOpRequestFilter.php b/src/ServerRequestFilter/DoNotFilter.php similarity index 78% rename from src/ServerRequestFilter/NoOpRequestFilter.php rename to src/ServerRequestFilter/DoNotFilter.php index c90f1efd..7a6867a8 100644 --- a/src/ServerRequestFilter/NoOpRequestFilter.php +++ b/src/ServerRequestFilter/DoNotFilter.php @@ -6,7 +6,7 @@ use Psr\Http\Message\ServerRequestInterface; -final class NoOpRequestFilter implements FilterServerRequestInterface +final class DoNotFilter implements FilterServerRequestInterface { public function __invoke(ServerRequestInterface $request): ServerRequestInterface { diff --git a/src/ServerRequestFilter/NoOpRequestFilterFactory.php b/src/ServerRequestFilter/NoOpRequestFilterFactory.php deleted file mode 100644 index 11355cca..00000000 --- a/src/ServerRequestFilter/NoOpRequestFilterFactory.php +++ /dev/null @@ -1,13 +0,0 @@ -assertSame($request, $filter($request)); } From 3d210e323fa8ef185a33428589dd1cbe3949c6b0 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 11:39:25 -0500 Subject: [PATCH 32/45] refactor: renames XForwardedRequestFilter to FilterUsingXForwardedHeaders Uses verb nomenclature. Also removes binding in `ConfigProvider`, as it was incorrect (was showing as an invokable, not a factory), and we will be removing it anyways. Signed-off-by: Matthew Weier O'Phinney --- src/ConfigProvider.php | 1 - src/ServerRequestFactory.php | 6 +- ...r.php => FilterUsingXForwardedHeaders.php} | 4 +- ...> FilterUsingXForwardedHeadersFactory.php} | 16 ++-- ...lterUsingXForwardedHeadersFactoryTest.php} | 86 +++++++++---------- ...p => FilterUsingXForwardedHeadersTest.php} | 32 +++---- 6 files changed, 72 insertions(+), 73 deletions(-) rename src/ServerRequestFilter/{XForwardedRequestFilter.php => FilterUsingXForwardedHeaders.php} (97%) rename src/ServerRequestFilter/{XForwardedRequestFilterFactory.php => FilterUsingXForwardedHeadersFactory.php} (69%) rename test/ServerRequestFilter/{XForwardedRequestFilterFactoryTest.php => FilterUsingXForwardedHeadersFactoryTest.php} (75%) rename test/ServerRequestFilter/{XForwardedRequestFilterTest.php => FilterUsingXForwardedHeadersTest.php} (86%) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index baae3b46..78afa047 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -44,7 +44,6 @@ public function getDependencies() : array ResponseFactoryInterface::class => ResponseFactory::class, StreamFactoryInterface::class => StreamFactory::class, ServerRequestFactoryInterface::class => ServerRequestFactory::class, - ServerRequestFilter\XForwardedRequestFilter::class => ServerRequestFilter\XForwardedRequestFilterFactory::class, UploadedFileFactoryInterface::class => UploadedFileFactory::class, UriFactoryInterface::class => UriFactory::class ], diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index ef61b590..ad8c8a67 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -4,8 +4,8 @@ namespace Laminas\Diactoros; -use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface; +use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders; use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; @@ -48,7 +48,7 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * generated request will be passed to this instance and the result * returned by this method. When not present, a default instance * is created and used. For version 2, that instance is an - * XForwardedRequestFilter, using the `trustAny()` constructor. + * FilterUsingXForwardedHeaders, using the `trustAny()` constructor. * For version 3, it will be a DoNotFilter instance. * @return ServerRequest */ @@ -61,7 +61,7 @@ public static function fromGlobals( ?FilterServerRequestInterface $requestFilter = null ) : ServerRequest { // @todo For version 3, we should instead create a DoNotFilter instance. - $requestFilter = $requestFilter ?: XForwardedRequestFilter::trustAny(); + $requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustAny(); $server = normalizeServer( $server ?: $_SERVER, diff --git a/src/ServerRequestFilter/XForwardedRequestFilter.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php similarity index 97% rename from src/ServerRequestFilter/XForwardedRequestFilter.php rename to src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index eb072746..0b3a80c5 100644 --- a/src/ServerRequestFilter/XForwardedRequestFilter.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -8,7 +8,7 @@ use Laminas\Diactoros\Exception\InvalidProxyAddressException; use Psr\Http\Message\ServerRequestInterface; -final class XForwardedRequestFilter implements FilterServerRequestInterface +final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface { public const HEADER_HOST = 'X-FORWARDED-HOST'; public const HEADER_PORT = 'X-FORWARDED-PORT'; @@ -28,7 +28,7 @@ final class XForwardedRequestFilter implements FilterServerRequestInterface /** * @var string[] - * @psalm-var array + * @psalm-var array */ private $trustedHeaders = []; diff --git a/src/ServerRequestFilter/XForwardedRequestFilterFactory.php b/src/ServerRequestFilter/FilterUsingXForwardedHeadersFactory.php similarity index 69% rename from src/ServerRequestFilter/XForwardedRequestFilterFactory.php rename to src/ServerRequestFilter/FilterUsingXForwardedHeadersFactory.php index d6319ec5..5f8ea6bd 100644 --- a/src/ServerRequestFilter/XForwardedRequestFilterFactory.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeadersFactory.php @@ -7,15 +7,15 @@ use Laminas\Diactoros\ConfigProvider; use Psr\Container\ContainerInterface; -final class XForwardedRequestFilterFactory +final class FilterUsingXForwardedHeadersFactory { - public function __invoke(ContainerInterface $container): XForwardedRequestFilter + public function __invoke(ContainerInterface $container): FilterUsingXForwardedHeaders { $config = $container->get('config'); $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::X_FORWARDED] ?? []; if (! is_array($config) || empty($config)) { - return XForwardedRequestFilter::trustNone(); + return FilterUsingXForwardedHeaders::trustNone(); } $proxies = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_PROXIES, $config) @@ -29,22 +29,22 @@ public function __invoke(ContainerInterface $container): XForwardedRequestFilter || empty($proxies) ) { // Makes no sense to set trusted headers if no proxies are trusted - return XForwardedRequestFilter::trustNone(); + return FilterUsingXForwardedHeaders::trustNone(); } // Missing trusted headers setting means all headers are considered trusted $headers = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_HEADERS, $config) ? $config[ConfigProvider::X_FORWARDED_TRUSTED_HEADERS] - : XForwardedRequestFilter::X_FORWARDED_HEADERS; + : FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS; if (! is_array($headers)) { // Invalid value - return XForwardedRequestFilter::trustNone(); + return FilterUsingXForwardedHeaders::trustNone(); } // Empty headers list implies trust all - $headers = empty($headers) ? XForwardedRequestFilter::X_FORWARDED_HEADERS : $headers; + $headers = empty($headers) ? FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS : $headers; - return XForwardedRequestFilter::trustProxies($proxies, $headers); + return FilterUsingXForwardedHeaders::trustProxies($proxies, $headers); } } diff --git a/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php b/test/ServerRequestFilter/FilterUsingXForwardedHeadersFactoryTest.php similarity index 75% rename from test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php rename to test/ServerRequestFilter/FilterUsingXForwardedHeadersFactoryTest.php index b4166f12..e0cba140 100644 --- a/test/ServerRequestFilter/XForwardedRequestFilterFactoryTest.php +++ b/test/ServerRequestFilter/FilterUsingXForwardedHeadersFactoryTest.php @@ -6,12 +6,12 @@ use Laminas\Diactoros\ConfigProvider; use Laminas\Diactoros\ServerRequest; -use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; -use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilterFactory; +use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders; +use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeadersFactory; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; -class XForwardedRequestFilterFactoryTest extends TestCase +class FilterUsingXForwardedHeadersFactoryTest extends TestCase { /** @var ContainerInterface */ private $container; @@ -70,13 +70,13 @@ public function randomIpGenerator(): iterable /** @dataProvider randomIpGenerator */ public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(string $remoteAddr): void { - $factory = new XForwardedRequestFilterFactory(); + $factory = new FilterUsingXForwardedHeadersFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -92,9 +92,9 @@ public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(str public function trustAnyProvider(): iterable { $headers = [ - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', - XForwardedRequestFilter::HEADER_PORT => '4443', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_PORT => '4443', ]; foreach ($this->randomIpGenerator() as $name => $arguments) { @@ -117,7 +117,7 @@ public function testIfWildcardProxyAddressSpecifiedReturnsFilterConfiguredToTrus ], ]); - $factory = new XForwardedRequestFilterFactory(); + $factory = new FilterUsingXForwardedHeadersFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( $headers, @@ -129,10 +129,10 @@ public function testIfWildcardProxyAddressSpecifiedReturnsFilterConfiguredToTrus $this->assertNotSame($request, $filteredRequest); $uri = $filteredRequest->getUri(); - $this->assertSame($headers[XForwardedRequestFilter::HEADER_HOST], $uri->getHost()); + $this->assertSame($headers[FilterUsingXForwardedHeaders::HEADER_HOST], $uri->getHost()); // Port is always cast to int - $this->assertSame((int) $headers[XForwardedRequestFilter::HEADER_PORT], $uri->getPort()); - $this->assertSame($headers[XForwardedRequestFilter::HEADER_PROTO], $uri->getScheme()); + $this->assertSame((int) $headers[FilterUsingXForwardedHeaders::HEADER_PORT], $uri->getPort()); + $this->assertSame($headers[FilterUsingXForwardedHeaders::HEADER_PROTO], $uri->getScheme()); } /** @dataProvider randomIpGenerator */ @@ -143,19 +143,19 @@ public function testEmptyProxiesListDoesNotTrustXForwardedRequests(string $remot ConfigProvider::X_FORWARDED => [ ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => [], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedRequestFilter::HEADER_HOST, + FilterUsingXForwardedHeaders::HEADER_HOST, ], ], ], ]); - $factory = new XForwardedRequestFilterFactory(); + $factory = new FilterUsingXForwardedHeadersFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -179,14 +179,14 @@ public function testEmptyHeadersListTrustsAllXForwardedRequestsForMatchedProxies ], ]); - $factory = new XForwardedRequestFilterFactory(); + $factory = new FilterUsingXForwardedHeadersFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest( [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', - XForwardedRequestFilter::HEADER_PORT => '4443', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_PORT => '4443', ], [ 'REMOTE_ADDR' => $remoteAddr, @@ -222,16 +222,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED => [ ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedRequestFilter::HEADER_HOST, + FilterUsingXForwardedHeaders::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', - XForwardedRequestFilter::HEADER_PORT => '4443', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -245,16 +245,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED => [ ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedRequestFilter::HEADER_HOST, + FilterUsingXForwardedHeaders::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', - XForwardedRequestFilter::HEADER_PORT => '4443', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -268,17 +268,17 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED => [ ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedRequestFilter::HEADER_HOST, - XForwardedRequestFilter::HEADER_PROTO, + FilterUsingXForwardedHeaders::HEADER_HOST, + FilterUsingXForwardedHeaders::HEADER_PROTO, ], ], ], ], [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', - XForwardedRequestFilter::HEADER_PORT => '4443', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.1.1'], 'http://localhost/foo/bar', @@ -292,16 +292,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED => [ ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedRequestFilter::HEADER_HOST, + FilterUsingXForwardedHeaders::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', - XForwardedRequestFilter::HEADER_PORT => '4443', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.2.1'], 'http://localhost/foo/bar', @@ -315,16 +315,16 @@ public function trustedProxiesAndHeaders(): iterable ConfigProvider::X_FORWARDED => [ ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - XForwardedRequestFilter::HEADER_HOST, + FilterUsingXForwardedHeaders::HEADER_HOST, ], ], ], ], [ 'Host' => 'localhost', - XForwardedRequestFilter::HEADER_HOST => 'api.example.com', - XForwardedRequestFilter::HEADER_PROTO => 'https', - XForwardedRequestFilter::HEADER_PORT => '4443', + FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', + FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', + FilterUsingXForwardedHeaders::HEADER_PORT => '4443', ], ['REMOTE_ADDR' => '192.168.2.1'], 'http://localhost/foo/bar', @@ -343,7 +343,7 @@ public function testCombinedProxiesAndHeadersDefineTrust( ): void { $this->container->set('config', $config); - $factory = new XForwardedRequestFilterFactory(); + $factory = new FilterUsingXForwardedHeadersFactory(); $filter = $factory($this->container); $request = $this->generateServerRequest($headers, $server, $baseUriString); diff --git a/test/ServerRequestFilter/XForwardedRequestFilterTest.php b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php similarity index 86% rename from test/ServerRequestFilter/XForwardedRequestFilterTest.php rename to test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php index 184279ee..40c6b6d7 100644 --- a/test/ServerRequestFilter/XForwardedRequestFilterTest.php +++ b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php @@ -6,11 +6,11 @@ use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException; use Laminas\Diactoros\Exception\InvalidProxyAddressException; -use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; +use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders; use Laminas\Diactoros\ServerRequest; use PHPUnit\Framework\TestCase; -class XForwardedRequestFilterTest extends TestCase +class FilterUsingXForwardedHeadersTest extends TestCase { public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllForwardedHeadersForThatProxy(): void { @@ -28,7 +28,7 @@ public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllF ] ); - $filter = XForwardedRequestFilter::trustProxies('192.168.1.0/24'); + $filter = FilterUsingXForwardedHeaders::trustProxies('192.168.1.0/24'); $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); @@ -54,9 +54,9 @@ public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThose ] ); - $filter = XForwardedRequestFilter::trustProxies( + $filter = FilterUsingXForwardedHeaders::trustProxies( '192.168.1.0/24', - [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] ); $filteredRequest = $filter($request); @@ -83,7 +83,7 @@ public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void ] ); - $filter = XForwardedRequestFilter::trustProxies('192.168.1.0/24'); + $filter = FilterUsingXForwardedHeaders::trustProxies('192.168.1.0/24'); $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); @@ -115,7 +115,7 @@ public function testTrustingProxyListWithoutExplicitTrustedHeadersTrustsAllForwa ] ); - $filter = XForwardedRequestFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); @@ -142,9 +142,9 @@ public function testTrustingProxyListWithSpecificTrustedHeadersTrustsOnlyThoseHe ] ); - $filter = XForwardedRequestFilter::trustProxies( + $filter = FilterUsingXForwardedHeaders::trustProxies( ['192.168.1.0/24', '10.1.0.0/16'], - [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] ); $filteredRequest = $filter($request); @@ -179,7 +179,7 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re ] ); - $filter = XForwardedRequestFilter::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24', '10.1.0.0/16']); $this->assertSame($request, $filter($request)); } @@ -187,19 +187,19 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re public function testPassingInvalidStringAddressForProxyRaisesException(): void { $this->expectException(InvalidProxyAddressException::class); - XForwardedRequestFilter::trustProxies('192.168.1'); + FilterUsingXForwardedHeaders::trustProxies('192.168.1'); } public function testPassingInvalidAddressInProxyListRaisesException(): void { $this->expectException(InvalidProxyAddressException::class); - XForwardedRequestFilter::trustProxies(['192.168.1']); + FilterUsingXForwardedHeaders::trustProxies(['192.168.1']); } public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void { $this->expectException(InvalidForwardedHeaderNameException::class); - XForwardedRequestFilter::trustProxies('192.168.1.0/24', ['Host']); + FilterUsingXForwardedHeaders::trustProxies('192.168.1.0/24', ['Host']); } public function testListOfForwardedHostsIsConsideredUntrusted(): void @@ -216,7 +216,7 @@ public function testListOfForwardedHostsIsConsideredUntrusted(): void ] ); - $filter = XForwardedRequestFilter::trustAny(); + $filter = FilterUsingXForwardedHeaders::trustAny(); $this->assertSame($request, $filter($request)); } @@ -235,7 +235,7 @@ public function testListOfForwardedPortsIsConsideredUntrusted(): void ] ); - $filter = XForwardedRequestFilter::trustAny(); + $filter = FilterUsingXForwardedHeaders::trustAny(); $this->assertSame($request, $filter($request)); } @@ -254,7 +254,7 @@ public function testListOfForwardedProtosIsConsideredUntrusted(): void ] ); - $filter = XForwardedRequestFilter::trustAny(); + $filter = FilterUsingXForwardedHeaders::trustAny(); $this->assertSame($request, $filter($request)); } From 9cf8ac29859a41641b4baa1a43d07871ea55df61 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 11:44:22 -0500 Subject: [PATCH 33/45] refactor: mark as immutable Also describes the purpose of the filter. Signed-off-by: Matthew Weier O'Phinney --- .../FilterUsingXForwardedHeaders.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index 0b3a80c5..f605d43a 100644 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -8,6 +8,16 @@ 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'; From e7e84c8bbf8ba380511af7e1f13cdcb8c634e8a1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 11:44:53 -0500 Subject: [PATCH 34/45] refactor: make `X_FORWARDED_HEADERS` private This becomes an internal detail, because an empty-set when calling `trustProxies()` is equivalent. Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/FilterUsingXForwardedHeaders.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index f605d43a..16e106ee 100644 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -24,7 +24,7 @@ final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface public const HEADER_PORT = 'X-FORWARDED-PORT'; public const HEADER_PROTO = 'X-FORWARDED-PROTO'; - public const X_FORWARDED_HEADERS = [ + private const X_FORWARDED_HEADERS = [ self::HEADER_HOST, self::HEADER_PORT, self::HEADER_PROTO, From b6a2f32bcf76d5e6ce698dcf45b0cdbf8105f5c0 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 12:07:29 -0500 Subject: [PATCH 35/45] refactor: tighten up arguments to `trustProxies()` - Renames `$proxies` to `$proxyCIDRList`. - Marks `$proxyCIDRList` as `list` - Allows for "*" to be used; equivalent to two entries of `0.0.0.0/0` and `::/0`. - Modifies `normalizeProxyList()` to perform the above. - Empty list is functionally equivalent to "trust nothing", so this allows removing the `$trustAny` property. - Modifies `trustNone()` to return the results of `new self()`. - Modifies `trustAny()` to return the results of `self::trustProxies(['*'])`. - Marks `$trustedHeaders` as `list` Signed-off-by: Matthew Weier O'Phinney --- .../FilterUsingXForwardedHeaders.php | 81 ++++++++++--------- .../FilterUsingXForwardedHeadersTest.php | 14 +--- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index 16e106ee..37738c6c 100644 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -31,34 +31,28 @@ final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface ]; /** - * @todo Toggle this to false for version 3.0. - * @var bool - */ - private $trustAny = true; - - /** - * @var string[] - * @psalm-var array + * @var list */ private $trustedHeaders = []; - /** @var string[] */ + /** @var list */ private $trustedProxies = []; /** * Do not trust any proxies, nor any X-FORWARDED-* headers. + * + * This is functionally equivalent to calling `trustProxies([], [])`. */ public static function trustNone(): self { - $filter = new self(); - $filter->trustAny = false; - - return $filter; + return new self(); } /** * 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 @@ -67,29 +61,31 @@ public static function trustNone(): self */ public static function trustAny(): self { - $filter = new self(); - $filter->trustAny = true; - $filter->trustedHeaders = self::X_FORWARDED_HEADERS; - - return $filter; + return self::trustProxies(['*']); } /** - * @param string|string[] $proxies - * @param array $trustedHeaders + * Indicate which proxies and which X-Forwarded headers to trust. + * + * @param list $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 $trustedHeaders If + * the list is empty, all X-Forwarded headers are trusted. * @throws InvalidProxyAddressException * @throws InvalidForwardedHeaderNameException */ public static function trustProxies( - $proxies, + array $proxyCIDRList, array $trustedHeaders = self::X_FORWARDED_HEADERS ): self { - $proxies = self::normalizeProxiesList($proxies); + $proxyCIDRList = self::normalizeProxiesList($proxyCIDRList); self::validateTrustedHeaders($trustedHeaders); $filter = new self(); - $filter->trustAny = false; - $filter->trustedProxies = $proxies; + $filter->trustedProxies = $proxyCIDRList; $filter->trustedHeaders = $trustedHeaders; return $filter; @@ -104,7 +100,7 @@ public function __invoke(ServerRequestInterface $request): ServerRequestInterfac return $request; } - if (! $this->trustAny && ! $this->isFromTrustedProxy($remoteAddress)) { + if (! $this->isFromTrustedProxy($remoteAddress)) { // Do nothing return $request; } @@ -142,10 +138,6 @@ public function __invoke(ServerRequestInterface $request): ServerRequestInterfac private function isFromTrustedProxy(string $remoteAddress): bool { - if ($this->trustAny) { - return true; - } - foreach ($this->trustedProxies as $proxy) { if (IPRange::matches($remoteAddress, $proxy)) { return true; @@ -165,22 +157,33 @@ private static function validateTrustedHeaders(array $headers): void } } - /** @throws InvalidProxyAddressException */ - private static function normalizeProxiesList($proxies): array + /** + * @param non-empty-list $proxyCIDRList + * @return non-empty-list + * @throws InvalidProxyAddressException + */ + private static function normalizeProxiesList(array $proxyCIDRList): array { - if (! is_array($proxies) && ! is_string($proxies)) { - throw InvalidProxyAddressException::forInvalidProxyArgument($proxies); - } + $foundWildcard = false; - $proxies = is_array($proxies) ? $proxies : [$proxies]; + foreach ($proxyCIDRList as $index => $cidr) { + if ($cidr === '*') { + unset($proxyCIDRList[$index]); + $foundWildcard = true; + continue; + } - foreach ($proxies as $proxy) { - if (! self::validateProxyCIDR($proxy)) { - throw InvalidProxyAddressException::forAddress($proxy); + if (! self::validateProxyCIDR($cidr)) { + throw InvalidProxyAddressException::forAddress($cidr); } } - return $proxies; + if ($foundWildcard) { + $proxyCIDRList[] = '0.0.0.0/0'; + $proxyCIDRList[] = '::/0'; + } + + return $proxyCIDRList; } /** diff --git a/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php index 40c6b6d7..af9f28a3 100644 --- a/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php +++ b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php @@ -28,7 +28,7 @@ public function testTrustingStringProxyWithoutSpecifyingTrustedHeadersTrustsAllF ] ); - $filter = FilterUsingXForwardedHeaders::trustProxies('192.168.1.0/24'); + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24']); $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); @@ -55,7 +55,7 @@ public function testTrustingStringProxyWithSpecificTrustedHeadersTrustsOnlyThose ); $filter = FilterUsingXForwardedHeaders::trustProxies( - '192.168.1.0/24', + ['192.168.1.0/24'], [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] ); @@ -83,7 +83,7 @@ public function testFilterDoesNothingWhenAddressNotFromTrustedProxy(): void ] ); - $filter = FilterUsingXForwardedHeaders::trustProxies('192.168.1.0/24'); + $filter = FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24']); $filteredRequest = $filter($request); $filteredUri = $filteredRequest->getUri(); @@ -184,12 +184,6 @@ public function testFilterDoesNothingWhenAddressNotInTrustedProxyList(string $re $this->assertSame($request, $filter($request)); } - public function testPassingInvalidStringAddressForProxyRaisesException(): void - { - $this->expectException(InvalidProxyAddressException::class); - FilterUsingXForwardedHeaders::trustProxies('192.168.1'); - } - public function testPassingInvalidAddressInProxyListRaisesException(): void { $this->expectException(InvalidProxyAddressException::class); @@ -199,7 +193,7 @@ public function testPassingInvalidAddressInProxyListRaisesException(): void public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void { $this->expectException(InvalidForwardedHeaderNameException::class); - FilterUsingXForwardedHeaders::trustProxies('192.168.1.0/24', ['Host']); + FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24'], ['Host']); } public function testListOfForwardedHostsIsConsideredUntrusted(): void From 9429abbf7e39e86aea6ff4fefe0e04ee1c11cae8 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 12:13:24 -0500 Subject: [PATCH 36/45] refactor: initialize properties in constructor, not named constructors Adds `$trustedProxies` and `$trustedHeaders` as optional array arguments to the constructor; `trustedProxies()` now passes those. Signed-off-by: Matthew Weier O'Phinney --- .../FilterUsingXForwardedHeaders.php | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index 37738c6c..c23ecdd9 100644 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -33,62 +33,20 @@ final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface /** * @var list */ - private $trustedHeaders = []; + private $trustedHeaders; /** @var list */ - private $trustedProxies = []; + private $trustedProxies; /** - * Do not trust any proxies, nor any X-FORWARDED-* headers. - * - * This is functionally equivalent to calling `trustProxies([], [])`. - */ - public static function trustNone(): self - { - return new self(); - } - - /** - * 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(['*']); - } - - /** - * Indicate which proxies and which X-Forwarded headers to trust. - * - * @param list $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 $trustedHeaders If - * the list is empty, all X-Forwarded headers are trusted. - * @throws InvalidProxyAddressException - * @throws InvalidForwardedHeaderNameException + * Only allow construction via named constructors */ - public static function trustProxies( - array $proxyCIDRList, - array $trustedHeaders = self::X_FORWARDED_HEADERS - ): self { - $proxyCIDRList = self::normalizeProxiesList($proxyCIDRList); - self::validateTrustedHeaders($trustedHeaders); - - $filter = new self(); - $filter->trustedProxies = $proxyCIDRList; - $filter->trustedHeaders = $trustedHeaders; - - return $filter; + private function __construct( + array $trustedProxies = [], + array $trustedHeaders = [] + ) { + $this->trustedProxies = $trustedProxies; + $this->trustedHeaders = $trustedHeaders; } public function __invoke(ServerRequestInterface $request): ServerRequestInterface @@ -136,6 +94,55 @@ public function __invoke(ServerRequestInterface $request): ServerRequestInterfac return $request; } + /** + * Do not trust any proxies, nor any X-FORWARDED-* headers. + * + * This is functionally equivalent to calling `trustProxies([], [])`. + */ + public static function trustNone(): self + { + return new self(); + } + + /** + * 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(['*']); + } + + /** + * Indicate which proxies and which X-Forwarded headers to trust. + * + * @param list $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 $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); + } + private function isFromTrustedProxy(string $remoteAddress): bool { foreach ($this->trustedProxies as $proxy) { @@ -224,11 +231,4 @@ private static function validateProxyCIDR($cidr): bool ) ); } - - /** - * Only allow construction via named constructors - */ - private function __construct() - { - } } From dcaf760c574954d253cb82f5492f4cc55b99698a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 12:41:59 -0500 Subject: [PATCH 37/45] feature: adds `trustReservedSubnets(array $trustedHeaders = [])` This patch adds a new name constructor, `trustReservedSubnets()`, which takes an optional argument, `$trustedHeaders`. Internally, it calls `trustProxies()` with the following list: - 10.0.0.0/8 (class-a subnet) - 127.0.0.0/8 (localhost addresses) - 172.16.0.0/12 (class-b subnet) - 192.168.0.0/16 (class-c subnet) - ::1/128 (ipv6 localhost) - fc00::/7 (ipv6 private networks) - fe80::/10 (ipv6 local-link addresses) Signed-off-by: Matthew Weier O'Phinney --- .../FilterUsingXForwardedHeaders.php | 61 +++++++++++---- .../FilterUsingXForwardedHeadersTest.php | 76 +++++++++++++++++++ 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index c23ecdd9..689cfb2f 100644 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -104,6 +104,29 @@ public static function trustNone(): self return new self(); } + /** + * Indicate which proxies and which X-Forwarded headers to trust. + * + * @param list $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 $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. * @@ -121,26 +144,34 @@ public static function trustAny(): self } /** - * Indicate which proxies and which X-Forwarded headers to trust. + * 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 $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 $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); + 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 diff --git a/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php index af9f28a3..1d23a1e0 100644 --- a/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php +++ b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php @@ -252,4 +252,80 @@ public function testListOfForwardedProtosIsConsideredUntrusted(): void $this->assertSame($request, $filter($request)); } + + /** @psalm-return iterable */ + public function trustedReservedNetworkList(): iterable + { + yield 'ipv4-localhost' => ['127.0.0.1']; + yield 'ipv4-class-a' => ['10.10.10.10']; + yield 'ipv4-class-b' => ['172.16.16.16']; + yield 'ipv4-class-c' => ['192.168.2.1']; + yield 'ipv6-localhost' => ['::1']; + yield 'ipv6-private' => ['fdb4:d239:27bc:1d9f:0001:0001:0001:0001']; + yield 'ipv6-local-link' => ['fe80:0000:0000:0000:abcd:abcd:abcd:abcd']; + } + + /** @dataProvider trustedReservedNetworkList */ + public function testTrustReservedSubnetsProducesFilterThatAcceptsAddressesFromThoseSubnets( + string $remoteAddr + ): void { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustReservedSubnets(); + + $filteredRequest = $filter($request); + $filteredUri = $filteredRequest->getUri(); + $this->assertNotSame($request->getUri(), $filteredUri); + $this->assertSame('example.com', $filteredUri->getHost()); + $this->assertSame(4433, $filteredUri->getPort()); + $this->assertSame('https', $filteredUri->getScheme()); + } + + /** @psalm-return iterable */ + public function unreservedNetworkAddressList(): iterable + { + yield 'ipv4-no-localhost' => ['128.0.0.1']; + yield 'ipv4-no-class-a' => ['19.10.10.10']; + yield 'ipv4-not-class-b' => ['173.16.16.16']; + yield 'ipv4-not-class-c' => ['193.168.2.1']; + yield 'ipv6-not-localhost' => ['::2']; + yield 'ipv6-not-private' => ['fab4:d239:27bc:1d9f:0001:0001:0001:0001']; + yield 'ipv6-not-local-link' => ['ef80:0000:0000:0000:abcd:abcd:abcd:abcd']; + } + + /** @dataProvider unreservedNetworkAddressList */ + public function testTrustReservedSubnetsProducesFilterThatRejectsAddressesNotFromThoseSubnets( + string $remoteAddr + ): void { + $request = new ServerRequest( + ['REMOTE_ADDR' => $remoteAddr], + [], + 'http://localhost:80/foo/bar', + 'GET', + 'php://temp', + [ + 'Host' => 'localhost', + 'X-Forwarded-Host' => 'example.com', + 'X-Forwarded-Port' => '4433', + 'X-Forwarded-Proto' => 'https', + ] + ); + + $filter = FilterUsingXForwardedHeaders::trustReservedSubnets(); + + $filteredRequest = $filter($request); + $this->assertSame($request, $filteredRequest); + } } From 80fc3dea74cb3a2bcbbf67de6dc7909b44149758 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 12:45:24 -0500 Subject: [PATCH 38/45] refactor: default FilterServerRequestInterface instance is now FilterUsingXForwardedHeaders::trustReservedSubnets This should keep us secure-by-default from the start. Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index ad8c8a67..735e81e6 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -48,7 +48,7 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * generated request will be passed to this instance and the result * returned by this method. When not present, a default instance * is created and used. For version 2, that instance is an - * FilterUsingXForwardedHeaders, using the `trustAny()` constructor. + * FilterUsingXForwardedHeaders, using the `trustReservedSubnets()` constructor. * For version 3, it will be a DoNotFilter instance. * @return ServerRequest */ @@ -61,7 +61,7 @@ public static function fromGlobals( ?FilterServerRequestInterface $requestFilter = null ) : ServerRequest { // @todo For version 3, we should instead create a DoNotFilter instance. - $requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustAny(); + $requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustReservedSubnets(); $server = normalizeServer( $server ?: $_SERVER, From 0aa29badd9d659253184c2756b3812527bf28cd4 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 12:52:32 -0500 Subject: [PATCH 39/45] qa: fixes as proposed by Marco - Better return value annotations - Removal of unnecessary note - Refactor to reduce need for temporary values Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFactory.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index 735e81e6..8a83dc9a 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -114,17 +114,14 @@ private static function marshalUriFromSapi(array $server) : Uri $uri = new Uri(''); // URI scheme - $scheme = 'http'; + $https = false; if (array_key_exists('HTTPS', $server)) { $https = self::marshalHttpsValue($server['HTTPS']); } elseif (array_key_exists('https', $server)) { $https = self::marshalHttpsValue($server['https']); - } else { - $https = false; } - $scheme = $https ? 'https' : $scheme; - $uri = $uri->withScheme($scheme); + $uri = $uri->withScheme($https ? 'https' : 'http'); // Set the host [$host, $port] = self::marshalHostAndPort($server); @@ -162,8 +159,8 @@ private static function marshalUriFromSapi(array $server) : Uri /** * Marshal the host and port from the PHP environment. * - * @return array Array of two items, host and port, in that order (can be - * passed to a list() operation). + * @return array{string, numeric-string} Array of two items, host and port, + * in that order (can be passed to a list() operation). */ private static function marshalHostAndPort(array $server) : array { @@ -188,8 +185,8 @@ private static function marshalHostAndPort(array $server) : array } /** - * @return array Array of two items, host and port, in that order (can be - * passed to a list() operation). + * @return array{string, numeric-string} Array of two items, host and port, + * in that order (can be passed to a list() operation). */ private static function marshalIpv6HostAndPort(array $server, ?int $port) : array { @@ -212,8 +209,6 @@ private static function marshalIpv6HostAndPort(array $server, ?int $port) : arra * - IIS7 UrlRewrite environment * - REQUEST_URI * - ORIG_PATH_INFO - * - * From Laminas\Http\PhpEnvironment\Request class */ private static function marshalRequestPath(array $server) : string { From 73dffa83da382dfcda0fc818aa427d6989226f89 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 12:54:22 -0500 Subject: [PATCH 40/45] refactor: remove X-Forwarded filter factory - Removes soft-dependency on PSR-11. - Moving to mezzio/mezzio, where it more rightly belongs. Signed-off-by: Matthew Weier O'Phinney --- .../FilterUsingXForwardedHeadersFactory.php | 50 --- ...ilterUsingXForwardedHeadersFactoryTest.php | 360 ------------------ 2 files changed, 410 deletions(-) delete mode 100644 src/ServerRequestFilter/FilterUsingXForwardedHeadersFactory.php delete mode 100644 test/ServerRequestFilter/FilterUsingXForwardedHeadersFactoryTest.php diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeadersFactory.php b/src/ServerRequestFilter/FilterUsingXForwardedHeadersFactory.php deleted file mode 100644 index 5f8ea6bd..00000000 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeadersFactory.php +++ /dev/null @@ -1,50 +0,0 @@ -get('config'); - $config = $config[ConfigProvider::CONFIG_KEY][ConfigProvider::X_FORWARDED] ?? []; - - if (! is_array($config) || empty($config)) { - return FilterUsingXForwardedHeaders::trustNone(); - } - - $proxies = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_PROXIES, $config) - ? $config[ConfigProvider::X_FORWARDED_TRUSTED_PROXIES] - : []; - - // '*' means trust any source as a trusted proxy for purposes of this factory - $proxies = $proxies === '*' ? ['0.0.0.0/0'] : $proxies; - - if ((! is_string($proxies) && ! is_array($proxies)) - || empty($proxies) - ) { - // Makes no sense to set trusted headers if no proxies are trusted - return FilterUsingXForwardedHeaders::trustNone(); - } - - // Missing trusted headers setting means all headers are considered trusted - $headers = array_key_exists(ConfigProvider::X_FORWARDED_TRUSTED_HEADERS, $config) - ? $config[ConfigProvider::X_FORWARDED_TRUSTED_HEADERS] - : FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS; - - if (! is_array($headers)) { - // Invalid value - return FilterUsingXForwardedHeaders::trustNone(); - } - - // Empty headers list implies trust all - $headers = empty($headers) ? FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS : $headers; - - return FilterUsingXForwardedHeaders::trustProxies($proxies, $headers); - } -} diff --git a/test/ServerRequestFilter/FilterUsingXForwardedHeadersFactoryTest.php b/test/ServerRequestFilter/FilterUsingXForwardedHeadersFactoryTest.php deleted file mode 100644 index e0cba140..00000000 --- a/test/ServerRequestFilter/FilterUsingXForwardedHeadersFactoryTest.php +++ /dev/null @@ -1,360 +0,0 @@ -container = new class() implements ContainerInterface { - private $services = []; - - /** - * @param string $id - * @return bool - */ - public function has($id) - { - return array_key_exists($id, $this->services); - } - - /** - * @param string $id - * @return mixed - */ - public function get($id) - { - if (! array_key_exists($id, $this->services)) { - return null; - } - - return $this->services[$id]; - } - - /** @param mixed $value */ - public function set(string $id, $value): void - { - $this->services[$id] = $value; - } - }; - - $this->container->set('config', []); - } - - public function generateServerRequest(array $headers, array $server, string $baseUrlString): ServerRequest - { - return new ServerRequest($server, [], $baseUrlString, 'GET', 'php://temp', $headers); - } - - /** @psalm-return iterable */ - public function randomIpGenerator(): iterable - { - yield 'class-a' => ['10.1.1.1']; - yield 'class-c' => ['192.168.1.1']; - yield 'localhost' => ['127.0.0.1']; - yield 'public' => ['4.4.4.4']; - } - - /** @dataProvider randomIpGenerator */ - public function testIfNoConfigPresentFactoryReturnsFilterThatDoesNotTrustAny(string $remoteAddr): void - { - $factory = new FilterUsingXForwardedHeadersFactory(); - $filter = $factory($this->container); - $request = $this->generateServerRequest( - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - ], - [ - 'REMOTE_ADDR' => $remoteAddr, - ], - 'http://localhost/foo/bar', - ); - - $filteredRequest = $filter($request); - $this->assertSame($request, $filteredRequest); - } - - /** @psalm-return iterable}> */ - public function trustAnyProvider(): iterable - { - $headers = [ - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - FilterUsingXForwardedHeaders::HEADER_PORT => '4443', - ]; - - foreach ($this->randomIpGenerator() as $name => $arguments) { - $arguments[] = $headers; - yield $name => $arguments; - } - } - - /** @dataProvider trustAnyProvider */ - public function testIfWildcardProxyAddressSpecifiedReturnsFilterConfiguredToTrustAny( - string $remoteAddr, - array $headers - ): void { - $headers['Host'] = 'localhost'; - $this->container->set('config', [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => '*', - ], - ], - ]); - - $factory = new FilterUsingXForwardedHeadersFactory(); - $filter = $factory($this->container); - $request = $this->generateServerRequest( - $headers, - ['REMOTE_ADDR' => $remoteAddr], - 'http://localhost/foo/bar', - ); - - $filteredRequest = $filter($request); - $this->assertNotSame($request, $filteredRequest); - - $uri = $filteredRequest->getUri(); - $this->assertSame($headers[FilterUsingXForwardedHeaders::HEADER_HOST], $uri->getHost()); - // Port is always cast to int - $this->assertSame((int) $headers[FilterUsingXForwardedHeaders::HEADER_PORT], $uri->getPort()); - $this->assertSame($headers[FilterUsingXForwardedHeaders::HEADER_PROTO], $uri->getScheme()); - } - - /** @dataProvider randomIpGenerator */ - public function testEmptyProxiesListDoesNotTrustXForwardedRequests(string $remoteAddr): void - { - $this->container->set('config', [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => [], - ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - FilterUsingXForwardedHeaders::HEADER_HOST, - ], - ], - ], - ]); - - $factory = new FilterUsingXForwardedHeadersFactory(); - $filter = $factory($this->container); - $request = $this->generateServerRequest( - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - ], - [ - 'REMOTE_ADDR' => $remoteAddr, - ], - 'http://localhost/foo/bar', - ); - - $filteredRequest = $filter($request); - $this->assertSame($request, $filteredRequest); - } - - /** @dataProvider randomIpGenerator */ - public function testEmptyHeadersListTrustsAllXForwardedRequestsForMatchedProxies(string $remoteAddr): void - { - $this->container->set('config', [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['0.0.0.0/0'], - ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [], - ], - ], - ]); - - $factory = new FilterUsingXForwardedHeadersFactory(); - $filter = $factory($this->container); - $request = $this->generateServerRequest( - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - FilterUsingXForwardedHeaders::HEADER_PORT => '4443', - ], - [ - 'REMOTE_ADDR' => $remoteAddr, - ], - 'http://localhost/foo/bar', - ); - - $filteredRequest = $filter($request); - $this->assertNotSame($request, $filteredRequest); - - $uri = $filteredRequest->getUri(); - $this->assertSame('api.example.com', $uri->getHost()); - $this->assertSame(4443, $uri->getPort()); - $this->assertSame('https', $uri->getScheme()); - } - - /** - * @psalm-return iterable>>, - * 2: array, - * 3: array, - * 4: string, - * 5: string - * }> - */ - public function trustedProxiesAndHeaders(): iterable - { - yield 'string-proxy-single-header' => [ - false, - [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => '192.168.1.1', - ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - FilterUsingXForwardedHeaders::HEADER_HOST, - ], - ], - ], - ], - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - FilterUsingXForwardedHeaders::HEADER_PORT => '4443', - ], - ['REMOTE_ADDR' => '192.168.1.1'], - 'http://localhost/foo/bar', - 'http://api.example.com/foo/bar', - ]; - - yield 'single-proxy-single-header' => [ - false, - [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], - ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - FilterUsingXForwardedHeaders::HEADER_HOST, - ], - ], - ], - ], - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - FilterUsingXForwardedHeaders::HEADER_PORT => '4443', - ], - ['REMOTE_ADDR' => '192.168.1.1'], - 'http://localhost/foo/bar', - 'http://api.example.com/foo/bar', - ]; - - yield 'single-proxy-multi-header' => [ - false, - [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], - ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - FilterUsingXForwardedHeaders::HEADER_HOST, - FilterUsingXForwardedHeaders::HEADER_PROTO, - ], - ], - ], - ], - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - FilterUsingXForwardedHeaders::HEADER_PORT => '4443', - ], - ['REMOTE_ADDR' => '192.168.1.1'], - 'http://localhost/foo/bar', - 'https://api.example.com/foo/bar', - ]; - - yield 'unmatched-proxy-single-header' => [ - true, - [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.1'], - ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - FilterUsingXForwardedHeaders::HEADER_HOST, - ], - ], - ], - ], - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - FilterUsingXForwardedHeaders::HEADER_PORT => '4443', - ], - ['REMOTE_ADDR' => '192.168.2.1'], - 'http://localhost/foo/bar', - 'http://localhost/foo/bar', - ]; - - yield 'matches-proxy-from-list-single-header' => [ - false, - [ - ConfigProvider::CONFIG_KEY => [ - ConfigProvider::X_FORWARDED => [ - ConfigProvider::X_FORWARDED_TRUSTED_PROXIES => ['192.168.1.0/24', '192.168.2.0/24'], - ConfigProvider::X_FORWARDED_TRUSTED_HEADERS => [ - FilterUsingXForwardedHeaders::HEADER_HOST, - ], - ], - ], - ], - [ - 'Host' => 'localhost', - FilterUsingXForwardedHeaders::HEADER_HOST => 'api.example.com', - FilterUsingXForwardedHeaders::HEADER_PROTO => 'https', - FilterUsingXForwardedHeaders::HEADER_PORT => '4443', - ], - ['REMOTE_ADDR' => '192.168.2.1'], - 'http://localhost/foo/bar', - 'http://api.example.com/foo/bar', - ]; - } - - /** @dataProvider trustedProxiesAndHeaders */ - public function testCombinedProxiesAndHeadersDefineTrust( - bool $expectUnfiltered, - array $config, - array $headers, - array $server, - string $baseUriString, - string $expectedUriString - ): void { - $this->container->set('config', $config); - - $factory = new FilterUsingXForwardedHeadersFactory(); - $filter = $factory($this->container); - $request = $this->generateServerRequest($headers, $server, $baseUriString); - - $filteredRequest = $filter($request); - - if ($expectUnfiltered) { - $this->assertSame($request, $filteredRequest); - return; - } - - $this->assertNotSame($request, $filteredRequest); - $this->assertSame($expectedUriString, $filteredRequest->getUri()->__toString()); - } -} From 83a7fc5d3284c9ea4a2a71bcc3fb6e29c3c06ebe Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 12:55:32 -0500 Subject: [PATCH 41/45] refactor: mark IPRange class internal Signed-off-by: Matthew Weier O'Phinney --- src/ServerRequestFilter/IPRange.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ServerRequestFilter/IPRange.php b/src/ServerRequestFilter/IPRange.php index 868cf80f..824b394a 100644 --- a/src/ServerRequestFilter/IPRange.php +++ b/src/ServerRequestFilter/IPRange.php @@ -4,6 +4,7 @@ namespace Laminas\Diactoros\ServerRequestFilter; +/** @internal */ final class IPRange { /** From 5c407290b03950194668a490060cbdb0bf09554e Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 13:07:51 -0500 Subject: [PATCH 42/45] docs: update documentation to reflect changes following refactoring - Interface name and signature change - Class renaming - Addition of `trustReservedSubnets()` method, and its usage as default setting - Removal of factories Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/api.md | 10 +-- docs/book/v2/forward-migration.md | 12 +-- docs/book/v2/server-request-filters.md | 105 +++++++++---------------- 3 files changed, 46 insertions(+), 81 deletions(-) diff --git a/docs/book/v2/api.md b/docs/book/v2/api.md index ea0e2911..6f57e8b7 100644 --- a/docs/book/v2/api.md +++ b/docs/book/v2/api.md @@ -105,7 +105,7 @@ $jsonResponse = new JsonResponse($data, 422, [ ## ServerRequestFactory This static class can be used to marshal a `ServerRequest` instance from the PHP environment. -The primary entry point is `Laminas\Diactoros\ServerRequestFactory::fromGlobals(array $server, array $query, array $body, array $cookies, array $files, ?Laminas\Diactoros\RequestFilter\RequestFilterInterface $requestFilter)`. +The primary entry point is `Laminas\Diactoros\ServerRequestFactory::fromGlobals(array $server, array $query, array $body, array $cookies, array $files, ?Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface $requestFilter)`. This method will create a new `ServerRequest` instance with the data provided. Examples of usage are: @@ -128,16 +128,16 @@ $request = ServerRequestFactory::fromGlobals( ### Request Filters Since version 2.11.1, this method takes the additional optional argument `$requestFilter`. -This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface`](server-request-filters.md). -For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter`](server-request-filters.md#xforwardedrequestfilter) instance configured as follows: +This should be a `null` value, or an instance of [`Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface`](server-request-filters.md). +For version 2 releases, if a `null` is provided, internally the method will assign a [`Laminas\Diactoros\ServerRequestFilter\FilerUsingXForwardedHeaders`](server-request-filters.md#filterusingxforwardedheaders) instance configured as follows: ```php -$requestFilter = $requestFilter ?: XForwardedRequestFilter::trustAny(); +$requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustReservedSubnets(); ``` The request filter is called on the generated server request instance, and its result is returned from `fromGlobals()`. -**For version 3 releases, this method will switch to using a `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` by default.** +**For version 3 releases, this method will switch to using a `Laminas\Diactoros\ServerRequestFilter\DoNotFilter` by default.** If you are using this factory method directly, please be aware and update your code accordingly. ### ServerRequestFactory Helper Functions diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md index 10c68789..87d0d3d1 100644 --- a/docs/book/v2/forward-migration.md +++ b/docs/book/v2/forward-migration.md @@ -2,20 +2,20 @@ ## ServerRequestFilterInterface defaults -Introduced in version 2.11.1, the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. +Introduced in version 2.11.1, the `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface` is used by `ServerRequestFactory::fromGlobals()` to allow modifying the generated `ServerRequest` instance prior to returning it. The primary use case is to allow modifying the generated URI based on the presence of headers such as `X-Forwarded-Host`. When operating behind a reverse proxy, the `Host` header is often rewritten to the name of the node to which the request is being forwarded, and an `X-Forwarded-Host` header is generated with the original `Host` value to allow the server to determine the original host the request was intended for. (We have always examined the `X-Forwarded-Proto` header; as of 2.11.1, we also examine the `X-Forwarded-Port` header.) -To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter`. +To accommodate this use case, we created `Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders`. Due to potential security issues, it is generally best to only accept these headers if you trust the reverse proxy that has initiated the request. (This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) -`XForwardedRequestFilter` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. -To prevent backwards compatibility breaks, we use this filter by default, marked to trust any proxy. +`FilterUsingXForwardedHeaders` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. +To prevent backwards compatibility breaks, we use this filter by default, marked to trust **only proxies on private subnets**. However, **in version 3, we will use a no-op filter by default**. -Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` instance, and we recommend explicitly configuring this to utilize the `XForwardedRequestFilter` if you depend on this functionality. -If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` as the configured `ServerRequestFilterInterface` in your application immediately. +Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface` instance, and we recommend explicitly configuring this to utilize the `FilterUsingXForwardedHeaders` if you depend on this functionality. +If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\DoNotFilter` as the configured `FilterServerRequestInterface` in your application immediately. We will update this documentation with a link to the related functionality in mezzio/mezzio when it is published. diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 960e1110..5314a3c0 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -9,18 +9,18 @@ Common use cases include: - Generating and injecting a request ID. - Modifying the request URI based on headers provided (e.g., based on the `X-Forwarded-Host` or `X-Forwarded-Proto` headers). -## ServerRequestFilterInterface +## FilerServerRequestInterface -A request filter implements `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface`: +A request filter implements `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface`: ```php namespace Laminas\Diactoros\ServerRequestFilter; use Psr\Http\Message\ServerRequestInterface; -interface ServerRequestFilterInterface +interface FilterServerRequestInterface { - public function filterRequest(ServerRequestInterface $request): ServerRequestInterface; + public function __invoke(ServerRequestInterface $request): ServerRequestInterface; } ``` @@ -28,30 +28,14 @@ interface ServerRequestFilterInterface We provide the following implementations: -- `NoOpRequestFilter`: returns the provided `$request` verbatim. -- `XForwardedRequestFilter`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. +- `DoNotFilter`: returns the provided `$request` verbatim. +- `FilterUsingXForwardedHeaders`: if the originating request comes from a trusted proxy, examines the `X-Forwarded-*` headers, and returns the request instance with a URI instance that reflects those headers. -### NoOpRequestFilter +### DoNotFilter This filter returns the `$request` argument back verbatim when invoked. -#### NoOpRequestFilterFactory - -Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilter` via the `Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilterFactory` class. -Register it as follows: - -```php -$config = [ - 'dependencies' => [ - 'factories' => [ - \Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface::class => - \Laminas\Diactoros\ServerRequestFilter\NoOpRequestFilterFactory::class, - ], - ], -]; -``` - -### XForwardedRequestFilter +### FilterUsingXForwardedHeaders Servers behind a reverse proxy need mechanisms to determine the original URL requested. As such, reverse proxies have provided a number of mechanisms for delivering this information, with the use of `X-Forwarded-*` headers being the most prevalant. @@ -61,89 +45,70 @@ These include: - `X-Forwarded-Port`: the original port included in the `Host` header value. - `X-Forwarded-Proto`: the original URI scheme used to make the request (e.g., "http" or "https"). -`Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter` provides named constructors for choosing whether to never trust proxies, always trust proxies, or choose wich proxies and/or headers to trust in order to modify the URI composed in the request instance to match the original request. +`Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders` provides named constructors for choosing whether to never trust proxies, always trust proxies, or choose wich proxies and/or headers to trust in order to modify the URI composed in the request instance to match the original request. These named constructors are: -- `XForwardedRequestFilter::trustNone(): void`: when this method is called, the filter will not trust any proxies, and return the request back verbatim. -- `XForwardedRequestFilter::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. -- `XForwardedRequestFilterFactory::trustProxies(string|string[] $proxies, string[] $trustedHeaders = XForwardedRequestFilter::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. +- `FilterUsingXForwardedHeadersFactory::trustProxies(string[] $proxyCIDRList, string[] $trustedHeaders = FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. + Proxies may be specified by IP address, or using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) for subnets; both IPv4 and IPv6 are accepted. + The special string "*" will be translated to two entries, `0.0.0.0/0` and `::/0`. +- `FilterUsingXForwardedHeaders::trustNone(): void`: when this method is called, the filter will not trust any proxies, and return the request back verbatim. + It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies([], [])`. +- `FilterUsingXForwardedHeaders::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. + It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies(['*'])`. +- `FilterUsingXForwardedHeaders::trustReservedSubnets(): void`: when this method is called, the filter will trust requests made from reserved, private subnets. + It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies()` with the following elements in the `$proxyCIDRList`: + - 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) -When providing one or more proxies to `trustProxies()`, the values may be exact IP addresses, or subnets specified by [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). Internally, the filter checks the `REMOTE_ADDR` server parameter (as retrieved from `getServerParams()`) and compares it against each proxy listed; the first to match indicates trust. #### Constants -The `XForwardedRequestFilter` defines the following constants for use in specifying various headers: +The `FilterUsingXForwardedHeaders` defines the following constants for use in specifying various headers: - `HEADER_HOST`: corresponds to `X-Forwarded-Host`. - `HEADER_PORT`: corresponds to `X-Forwarded-Port`. - `HEADER_PROTO`: corresponds to `X-Forwarded-Proto`. -- `X_FORWARDED_HEADERS`: corresponds to an array consisting of all of the above constants. #### Example usage Trusting all `X-Forwarded-*` headers from any source: ```php -$filter = XForwardedRequestFilter::trustAny(); +$filter = FilterUsingXForwardedHeaders::trustAny(); ``` Trusting only the `X-Forwarded-Host` header from any source: ```php -$filter = XForwardedRequestFilter::trustProxies('0.0.0.0/0', [XForwardedRequestFilter::HEADER_HOST]); +$filter = FilterUsingXForwardedHeaders::trustProxies('0.0.0.0/0', [FilterUsingXForwardedHeaders::HEADER_HOST]); ``` -Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a Class C subnet: +Trusting the `X-Forwarded-Host` and `X-Forwarded-Proto` headers from a single Class C subnet: ```php -$filter = XForwardedRequestFilter::trustProxies( +$filter = FilterUsingXForwardedHeaders::trustProxies( '192.168.1.0/24', - [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] ); ``` Trusting the `X-Forwarded-Host` header from either a Class A or a Class C subnet: ```php -$filter = XForwardedRequestFilter::trustProxies( +$filter = FilterUsingXForwardedHeaders::trustProxies( ['10.1.1.0/16', '192.168.1.0/24'], - [XForwardedRequestFilter::HEADER_HOST, XForwardedRequestFilter::HEADER_PROTO] + [FilterUsingXForwardedHeaders::HEADER_HOST, FilterUsingXForwardedHeaders::HEADER_PROTO] ); ``` -#### XForwardedRequestFilterFactory - -Diactoros also ships with a factory for generating a `Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter` via the `Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilterFactory` class. -This factory looks for the following configuration in order to generate an instance: - -```php -$config = [ - 'laminas-diactoros' => [ - 'x-forwarded-request-filter' => [ - 'trusted-proxies' => string|string[], - 'trusted-headers' => string[], - ], - ], -]; -``` - -- The `trusted-proxies` value may be one of the following: - - The string "*". This indicates that all originating addresses are trusted. - - A string IP address or CIDR notation value indicating a trusted proxy server or subnet. - - An array of string IP addresses or CIDR notation values. -- The `trusted-headers` array should consist of one or more of the `X-Forwarded-Host`, `X-Forwarded-Port`, or `X-Forwarded-Proto` header names; the values are case insensitive. - When the configuration is omitted or the array is empty, the assumption is to honor all `X-Forwarded-*` headers for trusted proxies. - -Register the factory using the `Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface` key: +Trusting any `X-Forwarded-*` header from any private subnet: ```php -$config = [ - 'dependencies' => [ - 'factories' => [ - \Laminas\Diactoros\ServerRequestFilter\ServerRequestFilterInterface::class => - \Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilterFactory::class, - ], - ], -]; +$filter = FilterUsingXForwardedHeaders::trustReservedSubnets(); ``` From d94d8a86149a747ab858e1e82c78d9cffaec6ee2 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 15:13:15 -0500 Subject: [PATCH 43/45] refactor: remove `trustNone()` method It's equivalent to using the `DoNotFilter` method. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/server-request-filters.md | 2 -- .../FilterUsingXForwardedHeaders.php | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/docs/book/v2/server-request-filters.md b/docs/book/v2/server-request-filters.md index 5314a3c0..30eb2356 100644 --- a/docs/book/v2/server-request-filters.md +++ b/docs/book/v2/server-request-filters.md @@ -51,8 +51,6 @@ These named constructors are: - `FilterUsingXForwardedHeadersFactory::trustProxies(string[] $proxyCIDRList, string[] $trustedHeaders = FilterUsingXForwardedHeaders::X_FORWARDED_HEADERS): void`: when this method is called, only requests originating from the trusted proxy/ies will be considered, as well as only the headers specified. Proxies may be specified by IP address, or using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) for subnets; both IPv4 and IPv6 are accepted. The special string "*" will be translated to two entries, `0.0.0.0/0` and `::/0`. -- `FilterUsingXForwardedHeaders::trustNone(): void`: when this method is called, the filter will not trust any proxies, and return the request back verbatim. - It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies([], [])`. - `FilterUsingXForwardedHeaders::trustAny(): void`: when this method is called, the filter will trust requests from any origin, and use any of the above headers to modify the URI instance. It is functionally equivalent to `FilterUsingXForwardedHeaders::trustProxies(['*'])`. - `FilterUsingXForwardedHeaders::trustReservedSubnets(): void`: when this method is called, the filter will trust requests made from reserved, private subnets. diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index 689cfb2f..70d69fc2 100644 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -94,16 +94,6 @@ public function __invoke(ServerRequestInterface $request): ServerRequestInterfac return $request; } - /** - * Do not trust any proxies, nor any X-FORWARDED-* headers. - * - * This is functionally equivalent to calling `trustProxies([], [])`. - */ - public static function trustNone(): self - { - return new self(); - } - /** * Indicate which proxies and which X-Forwarded headers to trust. * From 4d0cf3e7e15f7db621fdb347e7e0587c60e9df2a Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 15:15:25 -0500 Subject: [PATCH 44/45] docs: remove references to new major v3 and changes to server request filtering Since the default is to trust reserved subnets, the defaults should be safe now. Signed-off-by: Matthew Weier O'Phinney --- docs/book/v2/forward-migration.md | 1 - src/ServerRequestFactory.php | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/book/v2/forward-migration.md b/docs/book/v2/forward-migration.md index 87d0d3d1..375ad512 100644 --- a/docs/book/v2/forward-migration.md +++ b/docs/book/v2/forward-migration.md @@ -13,7 +13,6 @@ Due to potential security issues, it is generally best to only accept these head (This value is found in `$_SERVER['REMOTE_ADDR']`, which is present as `$request->getServerParams()['REMOTE_ADDR']` within PSR-7 implementations.) `FilterUsingXForwardedHeaders` provides named constructors to allow you to trust these headers from any source (which has been the default behavior of Diactoros since the beginning), or to specify specific IP addresses or CIDR subnets to trust, along with which headers are trusted. To prevent backwards compatibility breaks, we use this filter by default, marked to trust **only proxies on private subnets**. -However, **in version 3, we will use a no-op filter by default**. Features will be added to the 3.11.0 version of [mezzio/mezzio](https://github.com/mezzio/mezzio) that will allow configuring the `Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface` instance, and we recommend explicitly configuring this to utilize the `FilterUsingXForwardedHeaders` if you depend on this functionality. If you **do not** need the functionality, we recommend specifying `Laminas\Diactoros\ServerRequestFilter\DoNotFilter` as the configured `FilterServerRequestInterface` in your application immediately. diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index 8a83dc9a..cc1bce8a 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -46,10 +46,9 @@ class ServerRequestFactory implements ServerRequestFactoryInterface * @param array $files $_FILES superglobal * @param null|FilterServerRequestInterface $requestFilter If present, the * generated request will be passed to this instance and the result - * returned by this method. When not present, a default instance - * is created and used. For version 2, that instance is an - * FilterUsingXForwardedHeaders, using the `trustReservedSubnets()` constructor. - * For version 3, it will be a DoNotFilter instance. + * returned by this method. When not present, a default instance of + * FilterUsingXForwardedHeaders is created, using the `trustReservedSubnets()` + * constructor. * @return ServerRequest */ public static function fromGlobals( @@ -60,7 +59,6 @@ public static function fromGlobals( array $files = null, ?FilterServerRequestInterface $requestFilter = null ) : ServerRequest { - // @todo For version 3, we should instead create a DoNotFilter instance. $requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustReservedSubnets(); $server = normalizeServer( From 4b5d1adb5bc6571ba4834793fc13f4eb46e7dc50 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 27 Jun 2022 15:45:47 -0500 Subject: [PATCH 45/45] qa: apply Psalm rules and update baseline Corrected a number of issues flagged by Psalm, primarily around ensuring we have expected types. Most "new" errors involve: - Calling on PSR-7 mutator methods from a "pure" function (since the filter is marked immutable) - Inability to infer mixed type values (where we know they're mixed) Signed-off-by: Matthew Weier O'Phinney --- psalm-baseline.xml | 34 ++++++++++++++++++- psalm.xml.dist | 15 ++++++++ .../InvalidForwardedHeaderNameException.php | 5 +-- src/ServerRequestFactory.php | 28 +++++++++------ .../FilterUsingXForwardedHeaders.php | 15 ++++---- src/ServerRequestFilter/IPRange.php | 3 ++ .../FilterUsingXForwardedHeadersTest.php | 3 ++ 7 files changed, 82 insertions(+), 21 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index fc5a8baf..21ba7271 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + null|callable @@ -242,13 +242,45 @@ + $headers['cookie'] + + $iisUrlRewritten + $requestUri + $unencodedUrl + + + array{string, int|null} + + + $defaults + + + ServerRequest + is_callable(self::$apacheRequestHeaders) + + + getHeaderLine + getServerParams + getUri + withHost + withPort + withScheme + withUri + + + $proxyCIDRList + + + list<non-empty-string> + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 9cee6e9e..0a854e54 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -15,11 +15,26 @@ + + + + + + + + + + + + + + + diff --git a/src/Exception/InvalidForwardedHeaderNameException.php b/src/Exception/InvalidForwardedHeaderNameException.php index 97083a47..1c446439 100644 --- a/src/Exception/InvalidForwardedHeaderNameException.php +++ b/src/Exception/InvalidForwardedHeaderNameException.php @@ -4,10 +4,11 @@ namespace Laminas\Diactoros\Exception; -use Laminas\Diactoros\ServerRequestFilter\XForwardedRequestFilter; +use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders; class InvalidForwardedHeaderNameException extends RuntimeException implements ExceptionInterface { + /** @param mixed $name */ public static function forHeader($name): self { if (! is_string($name)) { @@ -17,7 +18,7 @@ public static function forHeader($name): self return new self(sprintf( 'Invalid X-Forwarded-* header name "%s" provided to %s', $name, - XForwardedRequestFilter::class + FilterUsingXForwardedHeaders::class )); } } diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index cc1bce8a..8b9d5fda 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -139,7 +139,7 @@ private static function marshalUriFromSapi(array $server) : Uri // URI query $query = ''; if (isset($server['QUERY_STRING'])) { - $query = ltrim($server['QUERY_STRING'], '?'); + $query = ltrim((string) $server['QUERY_STRING'], '?'); } // URI fragment @@ -157,7 +157,7 @@ private static function marshalUriFromSapi(array $server) : Uri /** * Marshal the host and port from the PHP environment. * - * @return array{string, numeric-string} Array of two items, host and port, + * @return array{string, int|null} Array of two items, host and port, * in that order (can be passed to a list() operation). */ private static function marshalHostAndPort(array $server) : array @@ -168,7 +168,7 @@ private static function marshalHostAndPort(array $server) : array return $defaults; } - $host = $server['SERVER_NAME']; + $host = (string) $server['SERVER_NAME']; $port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null; if (! isset($server['SERVER_ADDR']) @@ -183,14 +183,20 @@ private static function marshalHostAndPort(array $server) : array } /** - * @return array{string, numeric-string} Array of two items, host and port, + * @return array{string, int|null} Array of two items, host and port, * in that order (can be passed to a list() operation). */ private static function marshalIpv6HostAndPort(array $server, ?int $port) : array { - $host = '[' . $server['SERVER_ADDR'] . ']'; - $port = $port ?: 80; - if ($port . ']' === substr($host, strrpos($host, ':') + 1)) { + $host = '[' . (string) $server['SERVER_ADDR'] . ']'; + $port = $port ?: 80; + $portSeparatorPos = strrpos($host, ':'); + + if (false === $portSeparatorPos) { + return [$host, $port]; + } + + if ($port . ']' === substr($host, $portSeparatorPos + 1)) { // The last digit of the IPv6-Address has been taken as port // Unset the port so the default port can be used $port = null; @@ -214,18 +220,18 @@ private static function marshalRequestPath(array $server) : string // (double slash problem). $iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null; $unencodedUrl = $server['UNENCODED_URL'] ?? ''; - if ('1' === $iisUrlRewritten && ! empty($unencodedUrl)) { + if ('1' === $iisUrlRewritten && is_string($unencodedUrl) && '' !== $unencodedUrl) { return $unencodedUrl; } $requestUri = $server['REQUEST_URI'] ?? null; - if ($requestUri !== null) { + if (is_string($requestUri)) { return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri); } - $origPathInfo = $server['ORIG_PATH_INFO'] ?? null; - if (empty($origPathInfo)) { + $origPathInfo = $server['ORIG_PATH_INFO'] ?? ''; + if (! is_string($origPathInfo) || '' === $origPathInfo) { return '/'; } diff --git a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php index 70d69fc2..fcac3753 100644 --- a/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php +++ b/src/ServerRequestFilter/FilterUsingXForwardedHeaders.php @@ -40,6 +40,9 @@ final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface /** * Only allow construction via named constructors + * + * @param list $trustedProxies + * @param list $trustedHeaders */ private function __construct( array $trustedProxies = [], @@ -53,7 +56,7 @@ public function __invoke(ServerRequestInterface $request): ServerRequestInterfac { $remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? ''; - if ('' === $remoteAddress) { + if ('' === $remoteAddress || ! is_string($remoteAddress)) { // Should we trigger a warning here? return $request; } @@ -77,13 +80,11 @@ public function __invoke(ServerRequestInterface $request): ServerRequestInterfac $uri = $uri->withHost($header); break; case self::HEADER_PORT: - $uri = $uri->withPort($header); + $uri = $uri->withPort((int) $header); break; case self::HEADER_PROTO: $uri = $uri->withScheme($header); break; - default: - break; } } @@ -186,8 +187,8 @@ private static function validateTrustedHeaders(array $headers): void } /** - * @param non-empty-list $proxyCIDRList - * @return non-empty-list + * @param list $proxyCIDRList + * @return list * @throws InvalidProxyAddressException */ private static function normalizeProxiesList(array $proxyCIDRList): array @@ -219,7 +220,7 @@ private static function normalizeProxiesList(array $proxyCIDRList): array */ private static function validateProxyCIDR($cidr): bool { - if (! is_string($cidr)) { + if (! is_string($cidr) || '' === $cidr) { return false; } diff --git a/src/ServerRequestFilter/IPRange.php b/src/ServerRequestFilter/IPRange.php index 824b394a..871dde84 100644 --- a/src/ServerRequestFilter/IPRange.php +++ b/src/ServerRequestFilter/IPRange.php @@ -14,6 +14,7 @@ private function __construct() { } + /** @psalm-pure */ public static function matches(string $ip, string $cidr): bool { if (false !== strpos($ip, ':')) { @@ -23,6 +24,7 @@ public static function matches(string $ip, string $cidr): bool return self::matchesIPv4($ip, $cidr); } + /** @psalm-pure */ public static function matchesIPv4(string $ip, string $cidr): bool { $mask = 32; @@ -52,6 +54,7 @@ public static function matchesIPv4(string $ip, string $cidr): bool ); } + /** @psalm-pure */ public static function matchesIPv6(string $ip, string $cidr): bool { $mask = 128; diff --git a/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php index 1d23a1e0..653d91d1 100644 --- a/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php +++ b/test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php @@ -193,6 +193,9 @@ public function testPassingInvalidAddressInProxyListRaisesException(): void public function testPassingInvalidForwardedHeaderNamesWhenTrustingProxyRaisesException(): void { $this->expectException(InvalidForwardedHeaderNameException::class); + /** + * @psalm-suppress InvalidArgument + */ FilterUsingXForwardedHeaders::trustProxies(['192.168.1.0/24'], ['Host']); }