fix: SSRF 内网判定改用 ipaddress 标准库,覆盖全部 IPv4/IPv6 私有段
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3m20s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m30s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m19s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m48s
AI Code Review / AI Code Review (pull_request) Failing after 3m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m1s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 446h26m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 446h28m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 446h29m5s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 446h29m7s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 446h31m45s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 446h31m46s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 446h31m48s

AI Code Review 第三轮指出:IPv6 链路本地地址使用字符串前缀匹配
(fe80/fe90/...)不严谨且易漏。

修复:新增 _is_private_or_reserved_host(),统一用标准库
ipaddress.ip_address().is_private/is_loopback/is_link_local/
is_reserved 判定,一次性准确覆盖:
- IPv4: 10/8、172.16/12、192.168/16、127/8、169.254/16、0.0.0.0
- IPv6: ::1、fc00::/7(ULA)、fe80::/10(链路本地)、保留段
删除手工字符串切片逻辑,可读性和正确性都更好。

36 个测试全部通过。
This commit is contained in:
xiaoxia
2026-08-29 00:49:11 +08:00
parent bde37af2bb
commit cf7e295f35
+20 -18
View File
@@ -8,6 +8,7 @@
from __future__ import annotations
import ipaddress
import logging
import re
from typing import Any, List, Optional
@@ -191,6 +192,23 @@ def _endpoint_host(value: str) -> str:
return (urlparse("//" + v).hostname or "").lower()
def _is_private_or_reserved_host(host: str) -> bool:
"""判断主机名是否为内网/回环/链路本地/保留地址(IPv4 与 IPv6 统一处理)。
使用标准库 ipaddress 判定;非 IP 主机名(如 localhost)单独处理。
"""
h = host.strip().lower()
if h in {"localhost", "0.0.0.0", "::", "::1"}:
return True
try:
addr = ipaddress.ip_address(h)
# is_private 覆盖 10/8、172.16/12、192.168/16、127/8、169.254/16、
# ::1、fc00::/7、fe80::/10 等全部私有/保留段
return bool(addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved)
except ValueError:
return False
def _is_trusted_media_url(url: str) -> bool:
"""校验 URL 是否指向受信任的存储域名(OSS bucket / 本地存储),防止 SSRF。
@@ -206,25 +224,9 @@ def _is_trusted_media_url(url: str) -> bool:
host = (parsed.hostname or "").lower()
if not host:
return False
# 显式拒绝内网/保留地址(IPv4 + IPv6)
if host in {"localhost", "0.0.0.0", "::", "::1"}:
# 拒绝一切内网/回环/链路本地/保留地址(IPv4 + IPv6,标准库判定)
if _is_private_or_reserved_host(host):
return False
if host.startswith(("127.", "10.", "192.168.", "169.254.")):
return False
# IPv6 本地/链路本地/唯一本地地址:[::1] / fe80:: / fc00::/7
if ":" in host and (
host == "::1"
or host.startswith(("fe80", "fe90", "fea0", "feb0", "fec0", "fed0", "fee0", "fef0"))
or host.startswith(("fc", "fd"))
):
return False
# 172.16.0.0/12
try:
parts = [int(p) for p in host.split(".")]
if len(parts) == 4 and parts[0] == 172 and 16 <= parts[1] <= 31:
return False
except ValueError:
pass
# 允许:自家 OSS bucket 域名(<bucket>.<endpoint>)或 endpoint 自身及其子域
try:
storage_svc = get_shared_storage_service()