fix: 强化 SSRF 防护、修复代理链路安全漏洞与代码质量问题

基于 Gemini Code Assist 审查建议,对代理链路进行全面安全加固与代码优化。

- [新增] `src/lib/server/ssrf.ts`: 使用 `dns.promises.lookup` 进行真实 IP 解析,
  替代原有的正则匹配,防御 DNS 重绑定、非十进制 IP 等绕过手段
- [新增] 为 `proxy/vod/m3u8`、`proxy/vod/key`、`video-proxy` 三个接口补齐 SSRF 校验,
  此前仅 `proxy-m3u8` 和 `proxy/vod/segment` 有防护
- [删除] `utils.ts` 中已弃用的 `isValidUrlForProxy` 函数

- 所有代理接口统一强制 SSRF 校验,不再仅限于 `source=directplay`

- 修复 `proxy/vod/segment` 中 `isCancelled` 为 `const` 导致流取消信号失效的问题
- 修复 `proxy-m3u8/route.ts` 中导入不存在的函数(`extractResolutionFromM3u8`, `filterAdsFromM3U8Default`, `resolveM3u8Links`)的构建错误
- 修复直链直连模式下 `fetchCurrentSourceVideoInfo` 使用 HLS.js (XHR) 探测视频分辨率
  触发 CORS 误报的问题,改为直接跳过探测
- 移除 `proxy/vod/segment` 中未使用的 `NextRequest` 导入

- [新增] `src/lib/server/proxy-headers.ts`: 抽取 CORS 响应头为共享工具函数,
  消除 `proxy/vod/segment`、`proxy/vod/key`、`proxy/vod/m3u8` 中重复代码
- 统一使用 `DIRECT_PLAY_SOURCE` 常量替代硬编码 `'directplay'` 字符串

- `src/lib/server/ssrf.ts`
- `src/lib/server/proxy-headers.ts`

- `/app/api/proxy-m3u8/route`
- `/app/api/proxy/vod/key/route`
- `/app/api/proxy/vod/m3u8/route`
- `/app/api/proxy/vod/segment/route`
- `/app/api/video-proxy/route`
- `/app/play/page`x
- `/lib/utils`
This commit is contained in:
Troray
2026-03-11 21:11:24 +08:00
parent 11dd787e73
commit 3e9fdbb7a0
9 changed files with 200 additions and 86 deletions
-44
View File
@@ -407,47 +407,3 @@ export function base58Decode(encoded: string): string {
return Buffer.from(bytes).toString('utf-8');
}
/**
* 校验提供给代理层的外部 URL 是否安全
* 防止 SSRF (服务端请求伪造) 访问内网私有地址
*/
export function isValidUrlForProxy(urlStr: string): boolean {
if (!urlStr) return false;
try {
const parsed = new URL(urlStr);
// 仅允许 http 和 https
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return false;
}
const { hostname } = parsed;
// 拦截明显的本地或特定地址
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '0.0.0.0' ||
hostname === '[::1]'
) {
return false;
}
// 通过正则拦截内网 IPv4 (10.x, 172.16-31.x, 192.168.x, 169.254.x)
const ipv4Regex = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
const match = hostname.match(ipv4Regex);
if (match) {
const parts = match.slice(1).map(Number);
if (parts[0] === 10) return false;
if (parts[0] === 127) return false;
if (parts[0] === 192 && parts[1] === 168) return false;
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false;
if (parts[0] === 169 && parts[1] === 254) return false;
}
return true;
} catch {
// 解析失败直接判定为不安全
return false;
}
}