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:
+76
-13
@@ -18,6 +18,46 @@ use App\Http\Controllers\Frontend\MessageController;
|
|||||||
use App\Http\Controllers\Frontend\VoiceCallController;
|
use App\Http\Controllers\Frontend\VoiceCallController;
|
||||||
use App\Http\Controllers\Frontend\DiscoverController;
|
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'])
|
Route::get('/media/{path}', [MediaController::class, 'show'])
|
||||||
->where('path', '.*')
|
->where('path', '.*')
|
||||||
->name('media.show');
|
->name('media.show');
|
||||||
@@ -428,12 +468,10 @@ Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
|
|||||||
$url = base64_decode($u, true);
|
$url = base64_decode($u, true);
|
||||||
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
|
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
|
||||||
|
|
||||||
// Block private/internal IPs (SSRF protection)
|
// 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) ?? "";
|
$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)
|
|
||||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
|
$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
|
// Referer: custom ref param > auto-detect from URL's own origin
|
||||||
$customRef = $request->query("ref", "");
|
$customRef = $request->query("ref", "");
|
||||||
@@ -460,27 +498,39 @@ Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
|
|||||||
}
|
}
|
||||||
$origin = rtrim($referer, '/');
|
$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) {
|
$fetchUpstream = function (string $url, array $headers) {
|
||||||
|
$hops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (! animexe_url_is_safe($url)) return ['', 0, '', 'blocked_by_ssrf_guard'];
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
CURLOPT_FOLLOWLOCATION => false,
|
||||||
CURLOPT_MAXREDIRS => 5,
|
|
||||||
CURLOPT_TIMEOUT => 15,
|
CURLOPT_TIMEOUT => 15,
|
||||||
CURLOPT_CONNECTTIMEOUT => 8,
|
CURLOPT_CONNECTTIMEOUT => 8,
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
CURLOPT_SSL_VERIFYHOST => false,
|
CURLOPT_SSL_VERIFYHOST => false,
|
||||||
CURLOPT_ENCODING => '',
|
CURLOPT_ENCODING => '',
|
||||||
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||||
CURLOPT_HTTPHEADER => $headers,
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
]);
|
]);
|
||||||
$content = curl_exec($ch);
|
$content = curl_exec($ch);
|
||||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||||
|
$location = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL);
|
||||||
$errNo = curl_errno($ch);
|
$errNo = curl_errno($ch);
|
||||||
$errStr = $errNo ? curl_strerror($errNo) : '';
|
$errStr = $errNo ? curl_strerror($errNo) : '';
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode >= 300 && $httpCode < 400 && $location !== '' && $hops < 5) {
|
||||||
|
$hops++;
|
||||||
|
$url = $location;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return [$content, $httpCode, $ctype, $errStr];
|
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";
|
$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);
|
$url = base64_decode($u, true);
|
||||||
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
|
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
|
||||||
|
|
||||||
|
// 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) ?? "";
|
$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);
|
|
||||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
|
$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
|
// Referer: custom ref param > auto-detect from URL's own origin
|
||||||
$customRef = $request->query("ref", "");
|
$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) {
|
$segFetch = function (string $url, array $headers) {
|
||||||
|
$hops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (! animexe_url_is_safe($url)) return ['', 0];
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
CURLOPT_FOLLOWLOCATION => false,
|
||||||
CURLOPT_MAXREDIRS => 5,
|
|
||||||
CURLOPT_TIMEOUT => 15,
|
CURLOPT_TIMEOUT => 15,
|
||||||
CURLOPT_CONNECTTIMEOUT => 8,
|
CURLOPT_CONNECTTIMEOUT => 8,
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
CURLOPT_SSL_VERIFYHOST => false,
|
CURLOPT_SSL_VERIFYHOST => false,
|
||||||
CURLOPT_ENCODING => '',
|
CURLOPT_ENCODING => '',
|
||||||
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
||||||
CURLOPT_HTTPHEADER => $headers,
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
]);
|
]);
|
||||||
$content = curl_exec($ch);
|
$content = curl_exec($ch);
|
||||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$location = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode >= 300 && $httpCode < 400 && $location !== '' && $hops < 5) {
|
||||||
|
$hops++;
|
||||||
|
$url = $location;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return [$content, $httpCode];
|
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";
|
$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) ?? '';
|
$host = parse_url($url, PHP_URL_HOST) ?? '';
|
||||||
$allowed = ['b-cdn.net', 'bunnycdn.com', 'aniziumserver.site', 'aniziumserver.com', 'aniziumserver.sbs', 'anizium.co'];
|
$allowed = ['b-cdn.net', 'bunnycdn.com', 'aniziumserver.site', 'aniziumserver.com', 'aniziumserver.sbs', 'anizium.co'];
|
||||||
$ok = false;
|
$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);
|
if (!$ok) abort(403);
|
||||||
try {
|
try {
|
||||||
$resp = \Illuminate\Support\Facades\Http::timeout(15)
|
$resp = \Illuminate\Support\Facades\Http::timeout(15)
|
||||||
|
|||||||
Reference in New Issue
Block a user