Security: harden SSRF guard on stream proxies, fix vtt-proxy allowlist
Resolve hosts to IPs and reject private/reserved/link-local ranges (incl. 169.254 cloud metadata) instead of matching hostname text, and re-validate every redirect hop by following them manually. Restrict curl to http(s). vtt-proxy allowlist now requires an exact host or a real subdomain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+109
-46
@@ -18,6 +18,46 @@ use App\Http\Controllers\Frontend\MessageController;
|
||||
use App\Http\Controllers\Frontend\VoiceCallController;
|
||||
use App\Http\Controllers\Frontend\DiscoverController;
|
||||
|
||||
if (! function_exists('animexe_url_is_safe')) {
|
||||
/**
|
||||
* SSRF guard for the server-side stream/subtitle proxies.
|
||||
*
|
||||
* Only http(s) URLs are allowed, and the host must resolve exclusively to
|
||||
* public IPs. Resolving (instead of matching the hostname text) is what
|
||||
* blocks link-local cloud metadata (169.254.169.254), loopback, private
|
||||
* ranges and hostnames deliberately pointed at them (DNS rebinding).
|
||||
*/
|
||||
function animexe_url_is_safe(?string $url): bool
|
||||
{
|
||||
if (! $url) return false;
|
||||
|
||||
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
|
||||
if (! in_array($scheme, ['http', 'https'], true)) return false;
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if (! $host) return false;
|
||||
|
||||
$ips = [];
|
||||
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||
$ips[] = $host;
|
||||
} else {
|
||||
foreach (@gethostbynamel($host) ?: [] as $ip) $ips[] = $ip;
|
||||
foreach (@dns_get_record($host, DNS_AAAA) ?: [] as $rec) {
|
||||
if (! empty($rec['ipv6'])) $ips[] = $rec['ipv6'];
|
||||
}
|
||||
}
|
||||
if (! $ips) return false;
|
||||
|
||||
foreach ($ips as $ip) {
|
||||
if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Route::get('/media/{path}', [MediaController::class, 'show'])
|
||||
->where('path', '.*')
|
||||
->name('media.show');
|
||||
@@ -428,12 +468,10 @@ Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
|
||||
$url = base64_decode($u, true);
|
||||
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
|
||||
|
||||
// Block private/internal IPs (SSRF protection)
|
||||
$host = parse_url($url, PHP_URL_HOST) ?? "";
|
||||
if (!$host || preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) abort(403);
|
||||
// Only allow HTTPS streaming URLs (m3u8 or ts segments)
|
||||
// SSRF guard — resolves the host and rejects private/reserved/link-local IPs
|
||||
if (! animexe_url_is_safe($url)) abort(403);
|
||||
$host = parse_url($url, PHP_URL_HOST) ?? "";
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
|
||||
if (!in_array($scheme, ['http', 'https'])) abort(403);
|
||||
|
||||
// Referer: custom ref param > auto-detect from URL's own origin
|
||||
$customRef = $request->query("ref", "");
|
||||
@@ -460,27 +498,39 @@ Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
|
||||
}
|
||||
$origin = rtrim($referer, '/');
|
||||
|
||||
// Tek istek yürüten yardımcı — farklı header setleriyle tekrar denemek için
|
||||
// Tek istek yürüten yardımcı — farklı header setleriyle tekrar denemek için.
|
||||
// Redirect'ler elle takip edilir ki her adım SSRF kontrolünden geçsin.
|
||||
$fetchUpstream = function (string $url, array $headers) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$errNo = curl_errno($ch);
|
||||
$errStr = $errNo ? curl_strerror($errNo) : '';
|
||||
curl_close($ch);
|
||||
return [$content, $httpCode, $ctype, $errStr];
|
||||
$hops = 0;
|
||||
while (true) {
|
||||
if (! animexe_url_is_safe($url)) return ['', 0, '', 'blocked_by_ssrf_guard'];
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$location = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL);
|
||||
$errNo = curl_errno($ch);
|
||||
$errStr = $errNo ? curl_strerror($errNo) : '';
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 300 && $httpCode < 400 && $location !== '' && $hops < 5) {
|
||||
$hops++;
|
||||
$url = $location;
|
||||
continue;
|
||||
}
|
||||
return [$content, $httpCode, $ctype, $errStr];
|
||||
}
|
||||
};
|
||||
|
||||
$UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||
@@ -580,10 +630,10 @@ Route::get("/stream/seg", function (\Illuminate\Http\Request $request) {
|
||||
$url = base64_decode($u, true);
|
||||
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST) ?? "";
|
||||
if (!$host || preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) abort(403);
|
||||
// SSRF guard — resolves the host and rejects private/reserved/link-local IPs
|
||||
if (! animexe_url_is_safe($url)) abort(403);
|
||||
$host = parse_url($url, PHP_URL_HOST) ?? "";
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
|
||||
if (!in_array($scheme, ['http', 'https'])) abort(403);
|
||||
|
||||
// Referer: custom ref param > auto-detect from URL's own origin
|
||||
$customRef = $request->query("ref", "");
|
||||
@@ -611,24 +661,36 @@ Route::get("/stream/seg", function (\Illuminate\Http\Request $request) {
|
||||
]);
|
||||
}
|
||||
|
||||
// CDN'den çek — önce Referer-only (oynatıcı taklidi), olmazsa Origin ekle
|
||||
// CDN'den çek — önce Referer-only (oynatıcı taklidi), olmazsa Origin ekle.
|
||||
// Redirect'ler elle takip edilir ki her adım SSRF kontrolünden geçsin.
|
||||
$segFetch = function (string $url, array $headers) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
return [$content, $httpCode];
|
||||
$hops = 0;
|
||||
while (true) {
|
||||
if (! animexe_url_is_safe($url)) return ['', 0];
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$location = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode >= 300 && $httpCode < 400 && $location !== '' && $hops < 5) {
|
||||
$hops++;
|
||||
$url = $location;
|
||||
continue;
|
||||
}
|
||||
return [$content, $httpCode];
|
||||
}
|
||||
};
|
||||
$segUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||
|
||||
@@ -686,7 +748,8 @@ Route::get('/vtt-proxy', function (\Illuminate\Http\Request $request) {
|
||||
$host = parse_url($url, PHP_URL_HOST) ?? '';
|
||||
$allowed = ['b-cdn.net', 'bunnycdn.com', 'aniziumserver.site', 'aniziumserver.com', 'aniziumserver.sbs', 'anizium.co'];
|
||||
$ok = false;
|
||||
foreach ($allowed as $a) { if (str_ends_with($host, $a)) { $ok = true; break; } }
|
||||
// Tam eşleşme veya gerçek alt-domain — "evilanizium.co" gibi sahte son ekler geçmez
|
||||
foreach ($allowed as $a) { if ($host === $a || str_ends_with($host, '.' . $a)) { $ok = true; break; } }
|
||||
if (!$ok) abort(403);
|
||||
try {
|
||||
$resp = \Illuminate\Support\Facades\Http::timeout(15)
|
||||
|
||||
Reference in New Issue
Block a user