mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
Security:
- Fixed unauthenticated SSRF in /api/stream/proxy and /api/stream/ts-proxy
- proxy allowlist had escape hatch via pathname.endsWith('.m3u8') bypass
- ts-proxy had no destination validation at all
- Both could fetch internal services (127.0.0.1, 169.254.169.254)
- Added streamProxy.helper.js with domain allowlist, private IP blocking,
and http(s)-only enforcement applied to both endpoints
- Removed dead megaplay-1.buzz; added vid-tube.site, norami.top,
kryntal.top, akirax.buzz to allowlist
- Added SECURITY.md with vulnerability reporting policy
Bug fix:
- Fixed dotenv.config() loading after static imports -- helpers reading
process.env at top level never received .env values. Changed to
import 'dotenv/config' as first import
Config:
- Added STREAM_PROXY_DOMAINS env var for custom allowlist
- Added STREAM_PROXY_REFERER env var for custom CDN Referer
143 lines
5.5 KiB
JavaScript
143 lines
5.5 KiB
JavaScript
/*
|
|
* ======= • ======= • ======= • ======= • =======• =======
|
|
* AniKotoAPI — streamProxy.helper.js
|
|
* Repository: https://github.com/Shineii86
|
|
*
|
|
* @description
|
|
* Request guard for the M3U8/TS streaming proxies. Validates that a
|
|
* caller-supplied URL points at a known streaming CDN before the server
|
|
* fetches it, so the proxy endpoints cannot be used to reach internal
|
|
* services (SSRF).
|
|
*
|
|
* @exports
|
|
* assertProxyableUrl
|
|
* streamRefererHeaders
|
|
*
|
|
* @author Shinei Nouzen
|
|
* @license MIT
|
|
* ======= • ======= • ======= • ======= • =======• =======
|
|
*/
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// PROXY ALLOWLIST
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// Registrable domains the proxies may fetch from. Subdomains are covered,
|
|
// so the apex is listed once and rotating hosts (cdn1., s2., …) still match.
|
|
// NOTE: megaplay-1.buzz was removed — it no longer resolves.
|
|
const DEFAULT_PROXY_DOMAINS = [
|
|
// Embed players
|
|
"megaplay.buzz",
|
|
"vidtube.site",
|
|
"vid-tube.site",
|
|
"vidplay.site",
|
|
// Stream / subtitle CDNs
|
|
"anipixcdn.co",
|
|
"norami.top",
|
|
"kryntal.top",
|
|
"akirax.buzz",
|
|
];
|
|
|
|
// Parse the allowlist from env or use defaults
|
|
function parseProxyDomains() {
|
|
const envDomains = process.env.STREAM_PROXY_DOMAINS;
|
|
if (!envDomains) return DEFAULT_PROXY_DOMAINS;
|
|
|
|
return envDomains
|
|
.split(",")
|
|
.map(d => d.trim().toLowerCase())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
const PROXY_DOMAINS = parseProxyDomains();
|
|
|
|
// Referer the stream CDNs expect. They answer 403 to the site origin
|
|
// (anikototv.to) and to no Referer at all — only the player origin passes.
|
|
const STREAM_REFERER = process.env.STREAM_PROXY_REFERER || "https://megaplay.buzz/";
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// PRIVATE ADDRESS DETECTION
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// NOTE: Blocks IP literals pointing at the host's own network. This is a
|
|
// second layer, not the primary control — the allowlist is. A hostname that
|
|
// passes the allowlist but resolves to a private address (DNS rebinding) is
|
|
// not caught here, since axios re-resolves the name when it connects.
|
|
const isPrivateAddress = (hostname) => {
|
|
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
|
|
// IPv6 loopback, link-local (fe80::/10) and unique-local (fc00::/7)
|
|
if (host === "::1" || host === "::") return true;
|
|
if (/^f[cd][0-9a-f]{2}:/.test(host)) return true;
|
|
if (/^fe[89ab][0-9a-f]:/.test(host)) return true;
|
|
|
|
// IPv4-mapped IPv6 — unwrap and fall through to the IPv4 checks
|
|
const mapped = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
|
const target = mapped || host;
|
|
|
|
const octets = target.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
if (!octets) return false;
|
|
|
|
const [a, b] = octets.slice(1).map(Number);
|
|
if (a === 0 || a === 10 || a === 127) return true; // this-network, private, loopback
|
|
if (a === 169 && b === 254) return true; // link-local / cloud metadata
|
|
if (a === 172 && b >= 16 && b <= 31) return true; // private
|
|
if (a === 192 && b === 168) return true; // private
|
|
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
|
|
return false;
|
|
};
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// URL GUARD
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
// ---- FEATURE: Proxy URL Validation ----
|
|
/**
|
|
* Validates a caller-supplied proxy target.
|
|
*
|
|
* @param {string} rawUrl - The URL from the request query
|
|
* @returns {{ ok: true, url: URL } | { ok: false, status: number, message: string }}
|
|
*
|
|
* @example
|
|
* const check = assertProxyableUrl(req.query.url);
|
|
* if (!check.ok) return res.status(check.status).json({ success: false, message: check.message });
|
|
*/
|
|
const assertProxyableUrl = (rawUrl) => {
|
|
let url;
|
|
try {
|
|
url = new URL(rawUrl);
|
|
} catch {
|
|
return { ok: false, status: 400, message: "Malformed URL" };
|
|
}
|
|
|
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
return { ok: false, status: 400, message: "Only http and https URLs can be proxied" };
|
|
}
|
|
|
|
const hostname = url.hostname.toLowerCase();
|
|
|
|
if (isPrivateAddress(hostname)) {
|
|
return { ok: false, status: 403, message: "Domain not allowed for proxy" };
|
|
}
|
|
|
|
const isAllowed = PROXY_DOMAINS.some(d => hostname === d || hostname.endsWith(`.${d}`));
|
|
if (!isAllowed) {
|
|
return { ok: false, status: 403, message: "Domain not allowed for proxy" };
|
|
}
|
|
|
|
return { ok: true, url };
|
|
};
|
|
|
|
// ---- FEATURE: CDN Request Headers ----
|
|
/**
|
|
* Headers the stream CDNs require on a proxied fetch.
|
|
*
|
|
* @returns {object} Referer/Origin pair for the configured player origin
|
|
*/
|
|
const streamRefererHeaders = () => ({
|
|
"Referer": STREAM_REFERER,
|
|
"Origin": new URL(STREAM_REFERER).origin,
|
|
});
|
|
|
|
export { assertProxyableUrl, streamRefererHeaders };
|