mirror of https://github.com/pixelfed/pixelfed
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
140 lines
4.9 KiB
PHP
140 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\ActivityPub;
|
|
|
|
use App\Services\SecureMediaFetchService;
|
|
use App\Util\ActivityPub\Helpers;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Regression tests for the SSRF hardening in the remote media/avatar fetch
|
|
* path (variant of CVE-2026-71246).
|
|
*
|
|
* These cover the deterministic, network-free guards:
|
|
* - isPublicIp() rejects private/reserved/link-local ranges (incl. the
|
|
* 169.254.169.254 cloud-metadata address).
|
|
* - normalizeHost() rejects IP literals and localhost domains.
|
|
* - validateUrl() rejects non-https, userinfo, control chars, backslashes
|
|
* and IP-literal URLs.
|
|
* - SecureMediaFetchService fails closed (no network call) for URLs that
|
|
* cannot pass validation.
|
|
*/
|
|
class SsrfUrlValidationTest extends TestCase
|
|
{
|
|
// ---- isPublicIp -------------------------------------------------------
|
|
|
|
public static function privateAndReservedIps(): array
|
|
{
|
|
return [
|
|
'aws/gcp metadata' => ['169.254.169.254'],
|
|
'link-local' => ['169.254.0.1'],
|
|
'loopback v4' => ['127.0.0.1'],
|
|
'rfc1918 10/8' => ['10.0.0.1'],
|
|
'rfc1918 172.16/12' => ['172.18.0.1'],
|
|
'rfc1918 192.168/16' => ['192.168.1.1'],
|
|
'loopback v6' => ['::1'],
|
|
'unique local v6' => ['fd00::1'],
|
|
'unspecified' => ['0.0.0.0'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('privateAndReservedIps')]
|
|
public function it_rejects_private_and_reserved_ips(string $ip): void
|
|
{
|
|
$this->assertFalse(Helpers::isPublicIp($ip), $ip.' should be treated as non-public');
|
|
}
|
|
|
|
public static function publicIps(): array
|
|
{
|
|
return [
|
|
'cloudflare dns' => ['1.1.1.1'],
|
|
'google dns' => ['8.8.8.8'],
|
|
'public v6' => ['2606:4700:4700::1111'],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('publicIps')]
|
|
public function it_accepts_public_ips(string $ip): void
|
|
{
|
|
$this->assertTrue(Helpers::isPublicIp($ip), $ip.' should be public');
|
|
}
|
|
|
|
// ---- normalizeHost ----------------------------------------------------
|
|
|
|
#[Test]
|
|
public function it_rejects_ip_literal_hosts(): void
|
|
{
|
|
$this->assertNull(Helpers::normalizeHost('169.254.169.254'));
|
|
$this->assertNull(Helpers::normalizeHost('127.0.0.1'));
|
|
$this->assertNull(Helpers::normalizeHost('::1'));
|
|
}
|
|
|
|
#[Test]
|
|
public function it_rejects_localhost_domains(): void
|
|
{
|
|
$this->assertNull(Helpers::normalizeHost('localhost'));
|
|
}
|
|
|
|
#[Test]
|
|
public function it_normalizes_regular_hosts(): void
|
|
{
|
|
$this->assertSame('example.com', Helpers::normalizeHost('Example.com.'));
|
|
}
|
|
|
|
// ---- validateUrl ------------------------------------------------------
|
|
|
|
public static function invalidUrls(): array
|
|
{
|
|
return [
|
|
'http scheme' => ['http://example.com/avatar.jpg'],
|
|
'ftp scheme' => ['ftp://example.com/avatar.jpg'],
|
|
'ip literal https' => ['https://169.254.169.254/latest/meta-data/'],
|
|
'private ip literal' => ['https://172.18.0.1:9000/internal.jpg'],
|
|
'userinfo smuggling' => ['https://user:pass@example.com/a.jpg'],
|
|
'userinfo at-trick' => ['https://example.com@169.254.169.254/a.jpg'],
|
|
'control char' => ["https://example.com/\r\n/a.jpg"],
|
|
'backslash' => ['https://example.com\\@evil.com/a.jpg'],
|
|
'no dot host' => ['https://localhost/a.jpg'],
|
|
'empty' => [''],
|
|
];
|
|
}
|
|
|
|
#[Test]
|
|
#[DataProvider('invalidUrls')]
|
|
public function it_rejects_unsafe_urls(string $url): void
|
|
{
|
|
$this->assertFalse(Helpers::validateUrl($url), $url.' should be rejected');
|
|
}
|
|
|
|
#[Test]
|
|
public function it_accepts_a_normal_https_url_in_non_prod(): void
|
|
{
|
|
// In the local/testing environment shouldCheckDNS()/shouldCheckBans()
|
|
// are off, so a well-formed public https URL normalizes successfully.
|
|
$url = 'https://example.com/avatar.jpg';
|
|
$this->assertSame($url, Helpers::validateUrl($url));
|
|
}
|
|
|
|
// ---- SecureMediaFetchService fails closed -----------------------------
|
|
|
|
#[Test]
|
|
public function secure_fetch_head_fails_closed_on_invalid_url(): void
|
|
{
|
|
// These never resolve/connect because validateUrl rejects them first.
|
|
$this->assertFalse(SecureMediaFetchService::head('http://169.254.169.254/'));
|
|
$this->assertFalse(SecureMediaFetchService::head('https://172.18.0.1:9000/internal.jpg'));
|
|
$this->assertFalse(SecureMediaFetchService::head('https://example.com@169.254.169.254/a.jpg'));
|
|
}
|
|
|
|
#[Test]
|
|
public function secure_fetch_get_fails_closed_on_invalid_url(): void
|
|
{
|
|
$this->assertFalse(SecureMediaFetchService::get('http://169.254.169.254/'));
|
|
$this->assertFalse(SecureMediaFetchService::get('https://172.18.0.1:9000/internal.jpg'));
|
|
}
|
|
}
|