mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-11-03 19:08:50 +03:00
@ZaphodB fix(security): SSRF gate let through CGNAT, multicast and the server's own IPs
URIHasPublicHost() relied on FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE alone. On PHP 8.4 that passes 100.64.0.0/10 (shared space: CGNAT, Tailscale, Alibaba metadata at 100.100.100.200), 192.0.0.0/24, 198.18.0.0/15, 224.0.0.0/4, fec0::/10 and ff00::/8. It also passed the server's own public addresses: a fetch to them never leaves the host, so it reaches services that listen on every address but are firewalled from outside (on our host: grafana, forgejo, prometheus and the exporters). Both checks, on the literal host and on every resolved address, now go through IsPublicIP(): PHP's flags, then an explicit list of the ranges above, then the addresses net_get_interfaces() reports for this host (cached per request). test/ssrf-ranges.php checks the new ranges and their edges, that ordinary public addresses still pass, and the own-address rule via the cache. Before this change 100.64.0.1 was treated as public. DNS rebinding between check and connect is still not covered; that needs the connection pinned to the checked address.
This commit is contained in:
parent
8678c0e8fd
commit
650133ae8a
2 changed files with 352 additions and 0 deletions
40
test/ssrf-ranges.php
Normal file
40
test/ssrf-ranges.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
// Run with: php test/ssrf-ranges.php (needs no network: only IP literals)
|
||||
|
||||
// Enable SnappyMail Api and include index file
|
||||
$_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true;
|
||||
require __DIR__ . '/../index.php';
|
||||
|
||||
function check(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
$Request = 'SnappyMail\\Host';
|
||||
$public = fn(string $host) => $Request::URIHasPublicHost("http://{$host}/x.png");
|
||||
|
||||
foreach (['100.64.0.1', '100.100.100.200', '100.127.255.254', '192.0.0.1', '198.18.0.1',
|
||||
'198.19.255.254', '224.0.0.1', '239.255.255.250', '[fec0::1]', '[ff02::1]',
|
||||
'127.0.0.1', '10.0.0.1', '169.254.169.254', '[::1]', '[fc00::1]', '[::ffff:100.64.0.1]'] as $host) {
|
||||
check(!$public($host), "{$host} treated as public");
|
||||
}
|
||||
foreach (['100.63.255.255', '100.128.0.1', '198.17.255.255', '198.20.0.1', '8.8.8.8',
|
||||
'[2001:4860:4860::8888]'] as $host) {
|
||||
check($public($host), "{$host} wrongly blocked");
|
||||
}
|
||||
|
||||
// The server's own addresses. With --network=none only loopback exists, so
|
||||
// hand the cache an address to stand for this host's public IP.
|
||||
$prop = new ReflectionProperty($Request, 'aLocalAddresses');
|
||||
$prop->setValue(null, [inet_pton('203.0.113.7'), inet_pton('2001:db8:1::53')]);
|
||||
check(!$public('203.0.113.7'), "the server's own IPv4 address treated as public");
|
||||
check(!$public('[2001:db8:1::53]'), "the server's own IPv6 address treated as public");
|
||||
check($public('203.0.113.8'), 'a neighbouring address wrongly blocked');
|
||||
|
||||
check($public('snappymail.eu'), 'snapymail.eu wrongly blocked');
|
||||
check(!$public('localhost'), 'localhost should be blocked');
|
||||
|
||||
echo "ssrf-ranges: ok\n";
|
||||
Loading…
Add table
Add a link
Reference in a new issue