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
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Util\ActivityPub\Helpers;
|
|
|
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
|
|
|
use Illuminate\Http\Client\RequestException;
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
|
use League\Uri\BaseUri;
|
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SSRF-hardened fetcher for remote media (avatars, attachments).
|
|
|
|
|
*
|
|
|
|
|
* Unlike a bare Guzzle client or file_get_contents(), this service:
|
|
|
|
|
* - validates the URL (https-only, no userinfo, normalized host);
|
|
|
|
|
* - resolves the host and refuses any non-global (private/reserved/
|
|
|
|
|
* link-local) IP, closing the metadata.google.internal style bypass
|
|
|
|
|
* where a public-looking hostname resolves into a reserved range;
|
|
|
|
|
* - pins the connection to the already-validated IPs via CURLOPT_RESOLVE
|
|
|
|
|
* so the address we validated is the address we connect to (no
|
|
|
|
|
* TOCTOU / DNS-rebinding window);
|
|
|
|
|
* - never lets the HTTP client follow redirects automatically, instead
|
|
|
|
|
* re-validating and re-resolving every hop;
|
|
|
|
|
* - enforces a hard byte cap.
|
|
|
|
|
*/
|
|
|
|
|
class SecureMediaFetchService
|
|
|
|
|
{
|
|
|
|
|
private const int MAX_REDIRECTS = 2;
|
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
|
|
|
|
|
|
|
|
private const int CONNECT_TIMEOUT = 5;
|
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
|
|
|
|
|
|
|
|
private const int TIMEOUT = 15;
|
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
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Perform a HEAD request through the pinned, validated path.
|
|
|
|
|
*
|
|
|
|
|
* @return array{length:int,mime:string}|false
|
|
|
|
|
*/
|
|
|
|
|
public static function head(string $url, ?int $maxBytes = null)
|
|
|
|
|
{
|
|
|
|
|
$maxBytes = $maxBytes ?? self::defaultMaxBytes();
|
|
|
|
|
|
|
|
|
|
return (new self)->request($url, 'head', $maxBytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download up to $maxBytes of the resource through the pinned,
|
|
|
|
|
* validated path. Returns the raw body string, or false.
|
|
|
|
|
*
|
|
|
|
|
* @return string|false
|
|
|
|
|
*/
|
|
|
|
|
public static function get(string $url, ?int $maxBytes = null, ?int $expectedLength = null, array $headers = [])
|
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
|
|
|
{
|
|
|
|
|
$maxBytes = $maxBytes ?? self::defaultMaxBytes();
|
|
|
|
|
$result = (new self)->request($url, 'get', $maxBytes, $expectedLength, $headers);
|
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
|
|
|
|
|
|
|
|
if (! is_array($result)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result['body'] ?? false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shared request loop: validate -> resolve public IPs -> pin -> issue
|
|
|
|
|
* request with redirects disabled -> re-validate each hop.
|
|
|
|
|
*
|
|
|
|
|
* @return array|false For 'head': ['length'=>int,'mime'=>string].
|
|
|
|
|
* For 'get': ['body'=>string,'length'=>int,'mime'=>string].
|
|
|
|
|
*/
|
|
|
|
|
protected function request(string $url, string $method, int $maxBytes, ?int $expectedLength = null, array $extraHeaders = [])
|
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
|
|
|
{
|
|
|
|
|
$currentUrl = $url;
|
|
|
|
|
|
|
|
|
|
// Host of the original request. Caller-supplied headers (e.g. an
|
|
|
|
|
// Authorization bearer token) are only sent to this host and are
|
|
|
|
|
// stripped on any cross-origin redirect hop, mirroring Guzzle's
|
|
|
|
|
// RedirectMiddleware credential-stripping behaviour.
|
|
|
|
|
$originHost = parse_url($url, PHP_URL_HOST);
|
|
|
|
|
$originHost = is_string($originHost) ? strtolower($originHost) : null;
|
|
|
|
|
|
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
|
|
|
for ($redirects = 0; $redirects <= self::MAX_REDIRECTS; $redirects++) {
|
|
|
|
|
$currentUrl = Helpers::validateUrl($currentUrl);
|
|
|
|
|
|
|
|
|
|
if (! $currentUrl) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$host = parse_url($currentUrl, PHP_URL_HOST);
|
|
|
|
|
$scheme = parse_url($currentUrl, PHP_URL_SCHEME);
|
|
|
|
|
$port = parse_url($currentUrl, PHP_URL_PORT) ?: 443;
|
|
|
|
|
|
|
|
|
|
if (! $host || strtolower((string) $scheme) !== 'https') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only forward caller headers when the current hop is the same host
|
|
|
|
|
// as the original request; drop them across origins.
|
|
|
|
|
$headers = ['User-Agent' => self::userAgent()];
|
|
|
|
|
if (! empty($extraHeaders) && strtolower((string) $host) === $originHost) {
|
|
|
|
|
$headers = array_merge($extraHeaders, $headers);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Resolve the host and reject if ANY resolved address is
|
|
|
|
|
// non-global. Fail-closed: empty means unresolved or private.
|
|
|
|
|
$ips = Helpers::resolvePublicIps($host);
|
|
|
|
|
|
|
|
|
|
if (empty($ips)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$res = Http::withOptions([
|
|
|
|
|
'allow_redirects' => false,
|
|
|
|
|
'sink' => null,
|
|
|
|
|
'curl' => [
|
|
|
|
|
CURLOPT_RESOLVE => [
|
|
|
|
|
$this->buildResolveEntry($host, (int) $port, $ips),
|
|
|
|
|
],
|
|
|
|
|
CURLOPT_FRESH_CONNECT => true,
|
|
|
|
|
CURLOPT_FORBID_REUSE => true,
|
|
|
|
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
|
|
|
|
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
|
|
|
|
|
],
|
|
|
|
|
'on_headers' => function (ResponseInterface $response) use ($maxBytes) {
|
|
|
|
|
$length = $response->getHeaderLine('Content-Length');
|
|
|
|
|
if ($length !== '' && ctype_digit($length) && (int) $length > $maxBytes) {
|
|
|
|
|
throw new \RuntimeException('Remote media exceeds maximum size');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
->withHeaders($headers)
|
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
|
|
|
->timeout(self::TIMEOUT)
|
|
|
|
|
->connectTimeout(self::CONNECT_TIMEOUT)
|
|
|
|
|
->{$method}($currentUrl);
|
|
|
|
|
} catch (RequestException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
} catch (ConnectionException $e) {
|
|
|
|
|
return false;
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manual redirect handling: re-validate + re-resolve the next hop.
|
|
|
|
|
if (in_array($res->status(), [301, 302, 303, 307, 308], true)) {
|
|
|
|
|
if ($redirects >= self::MAX_REDIRECTS) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
$location = $res->header('Location');
|
|
|
|
|
if (! $location) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
$nextUrl = $this->resolveRedirect($currentUrl, $location);
|
|
|
|
|
if (! $nextUrl) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
$currentUrl = $nextUrl;
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! $res->successful()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$mime = $this->normalizeMime($res->header('Content-Type'));
|
|
|
|
|
$declaredLength = $res->header('Content-Length');
|
|
|
|
|
$declaredLength = ($declaredLength !== null && ctype_digit((string) $declaredLength))
|
|
|
|
|
? (int) $declaredLength
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if ($method === 'head') {
|
|
|
|
|
if ($declaredLength === null || $mime === null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if ($declaredLength < 10 || $declaredLength > $maxBytes) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['length' => $declaredLength, 'mime' => $mime];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET: enforce the cap against the actual body we received.
|
|
|
|
|
$body = $res->body();
|
|
|
|
|
$len = strlen($body);
|
|
|
|
|
|
|
|
|
|
if ($len === 0 || $len > $maxBytes) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($expectedLength !== null && $len < $expectedLength) {
|
|
|
|
|
// Received less than the HEAD promised; treat as truncated.
|
|
|
|
|
// Still return what we have, capped, but never more than asked.
|
|
|
|
|
$body = substr($body, 0, $expectedLength);
|
|
|
|
|
$len = strlen($body);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'body' => $body,
|
|
|
|
|
'length' => $len,
|
|
|
|
|
'mime' => $mime ?? ($declaredLength !== null ? $mime : null),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function buildResolveEntry(string $host, int $port, array $ips): string
|
|
|
|
|
{
|
|
|
|
|
$addresses = array_map(function ($ip) {
|
|
|
|
|
return str_contains($ip, ':') ? '['.$ip.']' : $ip;
|
|
|
|
|
}, $ips);
|
|
|
|
|
|
|
|
|
|
return $host.':'.$port.':'.implode(',', $addresses);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function resolveRedirect(string $baseUrl, string $location): ?string
|
|
|
|
|
{
|
|
|
|
|
$location = trim($location);
|
|
|
|
|
|
|
|
|
|
if ($location === '' || preg_match('/[\x00-\x20\x7f]/', $location)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$resolved = (string) BaseUri::from($baseUrl)->resolve($location);
|
|
|
|
|
|
|
|
|
|
return Helpers::validateUrl($resolved) ? $resolved : null;
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function normalizeMime(?string $contentType): ?string
|
|
|
|
|
{
|
|
|
|
|
if (! $contentType) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$mime = strtolower(trim(explode(';', $contentType)[0]));
|
|
|
|
|
|
|
|
|
|
return $mime === '' ? null : $mime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static function defaultMaxBytes(): int
|
|
|
|
|
{
|
|
|
|
|
// Cap on the larger of avatar/photo config limits (kB -> bytes),
|
|
|
|
|
// with a sane fallback.
|
|
|
|
|
$photo = (int) config_cache('pixelfed.max_photo_size');
|
|
|
|
|
$avatar = (int) config('pixelfed.max_avatar_size');
|
|
|
|
|
$maxKb = max($photo, $avatar, 1000);
|
|
|
|
|
|
|
|
|
|
return $maxKb * 1000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static function userAgent(): string
|
|
|
|
|
{
|
|
|
|
|
return 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')';
|
|
|
|
|
}
|
|
|
|
|
}
|