Skip to content

Commit

Permalink
feature: adds trustReservedSubnets(array $trustedHeaders = [])
Browse files Browse the repository at this point in the history
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 <matthew@weierophinney.net>
  • Loading branch information
weierophinney committed Jun 27, 2022
1 parent 9429abb commit dcaf760
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 15 deletions.
61 changes: 46 additions & 15 deletions src/ServerRequestFilter/FilterUsingXForwardedHeaders.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,29 @@ public static function trustNone(): self
return new self();
}

/**
* Indicate which proxies and which X-Forwarded headers to trust.
*
* @param list<non-empty-string> $proxyCIDRList Each element may
* be an IP address or a subnet specified using CIDR notation; both IPv4
* and IPv6 are supported. The special string "*" will be translated to
* two entries, "0.0.0.0/0" and "::/0". An empty list indicates no
* proxies are trusted.
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidProxyAddressException
* @throws InvalidForwardedHeaderNameException
*/
public static function trustProxies(
array $proxyCIDRList,
array $trustedHeaders = self::X_FORWARDED_HEADERS
): self {
$proxyCIDRList = self::normalizeProxiesList($proxyCIDRList);
self::validateTrustedHeaders($trustedHeaders);

return new self($proxyCIDRList, $trustedHeaders);
}

/**
* Trust any X-FORWARDED-* headers from any address.
*
Expand All @@ -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<non-empty-string> $proxyCIDRList Each element may
* be an IP address or a subnet specified using CIDR notation; both IPv4
* and IPv6 are supported. The special string "*" will be translated to
* two entries, "0.0.0.0/0" and "::/0". An empty list indicates no
* proxies are trusted.
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidProxyAddressException
* @throws InvalidForwardedHeaderNameException
*/
public static function trustProxies(
array $proxyCIDRList,
array $trustedHeaders = self::X_FORWARDED_HEADERS
): self {
$proxyCIDRList = self::normalizeProxiesList($proxyCIDRList);
self::validateTrustedHeaders($trustedHeaders);

return new self($proxyCIDRList, $trustedHeaders);
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
Expand Down
76 changes: 76 additions & 0 deletions test/ServerRequestFilter/FilterUsingXForwardedHeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,80 @@ public function testListOfForwardedProtosIsConsideredUntrusted(): void

$this->assertSame($request, $filter($request));
}

/** @psalm-return iterable<string, array{0: string}> */
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<string, array{0: string}> */
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);
}
}

0 comments on commit dcaf760

Please sign in to comment.