|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Util\ActivityPub\Helpers;
|
|
|
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
|
|
|
use Illuminate\Http\Client\RequestException;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
|
|
|
|
|
|
class FetchCacheService
|
|
|
|
|
{
|
|
|
|
|
const CACHE_KEY = 'pf:fetch_cache_service:getjson:';
|
|
|
|
|
|
|
|
|
|
public static function getJson($url, $verifyCheck = true, $ttl = 3600, $allowRedirects = true)
|
|
|
|
|
{
|
|
|
|
|
$vc = $verifyCheck ? 'vc1:' : 'vc0:';
|
|
|
|
|
$ar = $allowRedirects ? 'ar1:' : 'ar0';
|
|
|
|
|
$key = self::CACHE_KEY.sha1($url).':'.$vc.$ar.$ttl;
|
|
|
|
|
if (Cache::has($key)) {
|
|
|
|
|
return Cache::get($key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($verifyCheck) {
|
Fix unauthenticated SSRF in remote media/avatar fetch (variant of CVE-2026-71246)
The remote media path validated URLs only as strings (Helpers::validateUrl
normalizes the host + checks a ban list) and then downloaded them with
Http::head + file_get_contents($url), which resolve DNS themselves and
follow redirects with no private-IP checks and no address pinning. A remote
actor whose icon.url redirected to an internal address (e.g. 172.18.0.1 or
169.254.169.254) made the queue worker fetch internal content and, for
image responses, republish it at a public avatar URL. No account required.
Fixes:
- Add SecureMediaFetchService: validates URL, resolves + rejects non-global
IPs (fail-closed), pins the connection to the validated IP via
CURLOPT_RESOLVE, disables auto-redirects with per-hop re-validation, and
enforces https-only + a byte cap. Mirrors the ActivityPubFetchService
hardening from CVE-2026-71246.
- Route MediaStorageService head()/fetchAvatar()/remoteToCloud() through it,
removing the bare Http::head and file_get_contents($url) sinks.
- validateUrl(): when DNS verification is enabled, reject hosts that resolve
into reserved ranges, closing the metadata.google.internal bypass.
- Harden adjacent same-class sinks: CustomEmojiService (emoji doc + image +
head), FetchCacheService/webfinger, and DiscoverActor.
- Add regression tests (tests/Unit/ActivityPub/SsrfUrlValidationTest.php).
4 weeks ago
|
|
|
$validated = Helpers::validateUrl($url);
|
|
|
|
|
if (! $validated) {
|
|
|
|
|
Cache::put($key, 1, $ttl);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
Fix unauthenticated SSRF in remote media/avatar fetch (variant of CVE-2026-71246)
The remote media path validated URLs only as strings (Helpers::validateUrl
normalizes the host + checks a ban list) and then downloaded them with
Http::head + file_get_contents($url), which resolve DNS themselves and
follow redirects with no private-IP checks and no address pinning. A remote
actor whose icon.url redirected to an internal address (e.g. 172.18.0.1 or
169.254.169.254) made the queue worker fetch internal content and, for
image responses, republish it at a public avatar URL. No account required.
Fixes:
- Add SecureMediaFetchService: validates URL, resolves + rejects non-global
IPs (fail-closed), pins the connection to the validated IP via
CURLOPT_RESOLVE, disables auto-redirects with per-hop re-validation, and
enforces https-only + a byte cap. Mirrors the ActivityPubFetchService
hardening from CVE-2026-71246.
- Route MediaStorageService head()/fetchAvatar()/remoteToCloud() through it,
removing the bare Http::head and file_get_contents($url) sinks.
- validateUrl(): when DNS verification is enabled, reject hosts that resolve
into reserved ranges, closing the metadata.google.internal bypass.
- Harden adjacent same-class sinks: CustomEmojiService (emoji doc + image +
head), FetchCacheService/webfinger, and DiscoverActor.
- Add regression tests (tests/Unit/ActivityPub/SsrfUrlValidationTest.php).
4 weeks ago
|
|
|
$url = $validated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$headers = [
|
|
|
|
|
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')',
|
|
|
|
|
];
|
|
|
|
|
|
Fix unauthenticated SSRF in remote media/avatar fetch (variant of CVE-2026-71246)
The remote media path validated URLs only as strings (Helpers::validateUrl
normalizes the host + checks a ban list) and then downloaded them with
Http::head + file_get_contents($url), which resolve DNS themselves and
follow redirects with no private-IP checks and no address pinning. A remote
actor whose icon.url redirected to an internal address (e.g. 172.18.0.1 or
169.254.169.254) made the queue worker fetch internal content and, for
image responses, republish it at a public avatar URL. No account required.
Fixes:
- Add SecureMediaFetchService: validates URL, resolves + rejects non-global
IPs (fail-closed), pins the connection to the validated IP via
CURLOPT_RESOLVE, disables auto-redirects with per-hop re-validation, and
enforces https-only + a byte cap. Mirrors the ActivityPubFetchService
hardening from CVE-2026-71246.
- Route MediaStorageService head()/fetchAvatar()/remoteToCloud() through it,
removing the bare Http::head and file_get_contents($url) sinks.
- validateUrl(): when DNS verification is enabled, reject hosts that resolve
into reserved ranges, closing the metadata.google.internal bypass.
- Harden adjacent same-class sinks: CustomEmojiService (emoji doc + image +
head), FetchCacheService/webfinger, and DiscoverActor.
- Add regression tests (tests/Unit/ActivityPub/SsrfUrlValidationTest.php).
4 weeks ago
|
|
|
// SSRF-hardening: resolve the host and pin the connection to a
|
|
|
|
|
// validated public IP. Auto-redirects are disabled so a remote host
|
|
|
|
|
// cannot steer the request into an internal address on a later hop.
|
|
|
|
|
$host = parse_url($url, PHP_URL_HOST);
|
|
|
|
|
$port = parse_url($url, PHP_URL_PORT) ?: 443;
|
|
|
|
|
$ips = $host ? Helpers::resolvePublicIps($host) : [];
|
|
|
|
|
if ($ips === []) {
|
Fix unauthenticated SSRF in remote media/avatar fetch (variant of CVE-2026-71246)
The remote media path validated URLs only as strings (Helpers::validateUrl
normalizes the host + checks a ban list) and then downloaded them with
Http::head + file_get_contents($url), which resolve DNS themselves and
follow redirects with no private-IP checks and no address pinning. A remote
actor whose icon.url redirected to an internal address (e.g. 172.18.0.1 or
169.254.169.254) made the queue worker fetch internal content and, for
image responses, republish it at a public avatar URL. No account required.
Fixes:
- Add SecureMediaFetchService: validates URL, resolves + rejects non-global
IPs (fail-closed), pins the connection to the validated IP via
CURLOPT_RESOLVE, disables auto-redirects with per-hop re-validation, and
enforces https-only + a byte cap. Mirrors the ActivityPubFetchService
hardening from CVE-2026-71246.
- Route MediaStorageService head()/fetchAvatar()/remoteToCloud() through it,
removing the bare Http::head and file_get_contents($url) sinks.
- validateUrl(): when DNS verification is enabled, reject hosts that resolve
into reserved ranges, closing the metadata.google.internal bypass.
- Harden adjacent same-class sinks: CustomEmojiService (emoji doc + image +
head), FetchCacheService/webfinger, and DiscoverActor.
- Add regression tests (tests/Unit/ActivityPub/SsrfUrlValidationTest.php).
4 weeks ago
|
|
|
Cache::put($key, 1, $ttl);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
Fix unauthenticated SSRF in remote media/avatar fetch (variant of CVE-2026-71246)
The remote media path validated URLs only as strings (Helpers::validateUrl
normalizes the host + checks a ban list) and then downloaded them with
Http::head + file_get_contents($url), which resolve DNS themselves and
follow redirects with no private-IP checks and no address pinning. A remote
actor whose icon.url redirected to an internal address (e.g. 172.18.0.1 or
169.254.169.254) made the queue worker fetch internal content and, for
image responses, republish it at a public avatar URL. No account required.
Fixes:
- Add SecureMediaFetchService: validates URL, resolves + rejects non-global
IPs (fail-closed), pins the connection to the validated IP via
CURLOPT_RESOLVE, disables auto-redirects with per-hop re-validation, and
enforces https-only + a byte cap. Mirrors the ActivityPubFetchService
hardening from CVE-2026-71246.
- Route MediaStorageService head()/fetchAvatar()/remoteToCloud() through it,
removing the bare Http::head and file_get_contents($url) sinks.
- validateUrl(): when DNS verification is enabled, reject hosts that resolve
into reserved ranges, closing the metadata.google.internal bypass.
- Harden adjacent same-class sinks: CustomEmojiService (emoji doc + image +
head), FetchCacheService/webfinger, and DiscoverActor.
- Add regression tests (tests/Unit/ActivityPub/SsrfUrlValidationTest.php).
4 weeks ago
|
|
|
|
|
|
|
|
$options = [
|
|
|
|
|
'allow_redirects' => false,
|
|
|
|
|
'curl' => [
|
|
|
|
|
CURLOPT_RESOLVE => [
|
|
|
|
|
$host.':'.((int) $port).':'.implode(',', array_map(
|
|
|
|
|
fn ($ip) => str_contains($ip, ':') ? '['.$ip.']' : $ip,
|
|
|
|
|
$ips
|
|
|
|
|
)),
|
|
|
|
|
],
|
|
|
|
|
CURLOPT_FRESH_CONNECT => true,
|
|
|
|
|
CURLOPT_FORBID_REUSE => true,
|
|
|
|
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
|
|
|
|
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$res = Http::withOptions($options)
|
|
|
|
|
->retry(3, function (int $attempt, $exception) {
|
|
|
|
|
return $attempt * 500;
|
|
|
|
|
})
|
|
|
|
|
->acceptJson()
|
|
|
|
|
->withHeaders($headers)
|
|
|
|
|
->timeout(40)
|
|
|
|
|
->get($url);
|
|
|
|
|
} catch (RequestException|ConnectionException|\Exception) {
|
|
|
|
|
Cache::put($key, 1, $ttl);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! $res->ok()) {
|
|
|
|
|
Cache::put($key, 1, $ttl);
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result = $res->json();
|
|
|
|
|
Cache::put($key, $result, $ttl);
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
}
|