2048) { deny(400); } $parts = parse_url($raw); if ($parts === false || empty($parts['scheme']) || empty($parts['host'])) { deny(400); } $scheme = strtolower($parts['scheme']); if ($scheme !== 'http' && $scheme !== 'https') { deny(400); } $host = strtolower($parts['host']); if (!in_array($host, ALLOWED_HOSTS, true)) { deny(403); } // Second line of defence: even an allow-listed name must not resolve inward. $ip = gethostbyname($host); if ($ip === $host) { deny(502); // name did not resolve } if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { deny(403); // resolves to a private or reserved address } $ch = curl_init($raw); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, // a redirect could point inward CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => TIMEOUT_S, CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, CURLOPT_USERAGENT => 'LUPMIS2-legend-proxy/1.0', CURLOPT_ACCEPT_ENCODING => '', // Stop reading once the cap is passed, rather than buffering a huge body. CURLOPT_BUFFERSIZE => 16384, CURLOPT_NOPROGRESS => false, CURLOPT_PROGRESSFUNCTION => function ($res, $dlTotal, $dlNow) { return ($dlTotal > MAX_BYTES || $dlNow > MAX_BYTES) ? 1 : 0; }, ]); $body = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $type = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); if ($body === false || $status !== 200) { deny(502); } if (strlen($body) > MAX_BYTES) { deny(502); } // Only ever hand back an image, whatever the upstream claims to be. if (stripos($type, 'image/') !== 0) { deny(502); } $clean = explode(';', $type)[0]; header('Content-Type: ' . trim($clean)); header('Content-Length: ' . strlen($body)); header('Cache-Control: public, max-age=86400'); header('X-Content-Type-Options: nosniff'); header('Cross-Origin-Resource-Policy: same-origin'); echo $body;