9e10e31a29
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 23s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m17s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 15m19s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 15m19s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
P0-1 SSRF防护:统一URL安全校验方案 - 新增 url_security.py:SSRF防护工具(scheme白名单、端口白名单、内网IP拦截、内网主机名拦截、DNS解析校验) - generation.py URL校验函数增加SSRF前置校验 - PiP引擎URL类型增加安全校验 - 贴纸引擎URL类型增加安全校验 P0-2 路径遍历防护:输入路径校验强化 - 新增 path_security.py:路径安全工具(路径遍历防护、local:// schema校验、文件名清理、允许目录白名单) - resolve_asset_path 加固:本地绝对路径白名单、空字节检测、存储键路径遍历拦截 - PiP引擎local_path类型:增加允许目录校验 - 贴纸引擎:区分本地路径/URL,分别增加安全校验 P1 裸subprocess统一:3处FFmpeg调用走run_ffmpeg - unified_render_service.py _extract_audio:改用 run_ffmpeg - compose_video.py:改用 run_ffmpeg - edit_plan_generation.py:改用 run_ffmpeg 新增测试: - test_url_security.py:29个SSRF防护单测 - test_path_security.py:27个路径遍历防护单测 - 原有171+105=276个单测零回归
165 lines
6.9 KiB
Python
Executable File
165 lines
6.9 KiB
Python
Executable File
"""URL 安全校验工具单元测试 — SSRF 防护."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "apps", "worker"))
|
|
|
|
from video_processing.url_security import ( # noqa: E402
|
|
UrlSecurityError,
|
|
is_url_safe,
|
|
validate_url_safety,
|
|
)
|
|
|
|
|
|
class TestUrlSecurityValidation(unittest.TestCase):
|
|
"""URL 安全校验测试."""
|
|
|
|
# ── Scheme 白名单 ──────────────────────────────────────────────────────
|
|
|
|
def test_http_scheme_allowed(self):
|
|
"""HTTP scheme 应该被允许."""
|
|
result = validate_url_safety("http://example.com/test", purpose="test")
|
|
self.assertEqual(result, "http://example.com/test")
|
|
|
|
def test_https_scheme_allowed(self):
|
|
"""HTTPS scheme 应该被允许."""
|
|
result = validate_url_safety("https://example.com/test", purpose="test")
|
|
self.assertEqual(result, "https://example.com/test")
|
|
|
|
def test_file_scheme_rejected(self):
|
|
"""file:// scheme 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("file:///etc/passwd", purpose="test")
|
|
|
|
def test_ftp_scheme_rejected(self):
|
|
"""ftp:// scheme 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("ftp://example.com/test", purpose="test")
|
|
|
|
def test_empty_scheme_rejected(self):
|
|
"""空 scheme 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("example.com/test", purpose="test")
|
|
|
|
# ── 端口白名单 ────────────────────────────────────────────────────────
|
|
|
|
def test_port_80_allowed(self):
|
|
"""端口 80 应该被允许."""
|
|
# 80端口是默认HTTP端口,不显式指定也可以
|
|
result = validate_url_safety("http://example.com:80/test", purpose="test")
|
|
self.assertIn("example.com", result)
|
|
|
|
def test_port_443_allowed(self):
|
|
"""端口 443 应该被允许."""
|
|
result = validate_url_safety("https://example.com:443/test", purpose="test")
|
|
self.assertIn("example.com", result)
|
|
|
|
def test_port_8080_rejected(self):
|
|
"""非标准端口 8080 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://example.com:8080/test", purpose="test")
|
|
|
|
def test_port_22_rejected(self):
|
|
"""SSH 端口 22 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://example.com:22/test", purpose="test")
|
|
|
|
# ── SSRF: 直接 IP 访问 ───────────────────────────────────────────────
|
|
|
|
def test_loopback_ip_rejected(self):
|
|
"""回环地址 127.0.0.1 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://127.0.0.1/test", purpose="test")
|
|
|
|
def test_private_ip_192_rejected(self):
|
|
"""内网地址 192.168.x.x 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://192.168.1.1/test", purpose="test")
|
|
|
|
def test_private_ip_10_rejected(self):
|
|
"""内网地址 10.x.x.x 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://10.0.0.1/test", purpose="test")
|
|
|
|
def test_private_ip_172_rejected(self):
|
|
"""内网地址 172.16.x.x 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://172.16.0.1/test", purpose="test")
|
|
|
|
def test_unspecified_ip_rejected(self):
|
|
"""未指定地址 0.0.0.0 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://0.0.0.0/test", purpose="test")
|
|
|
|
def test_ipv6_loopback_rejected(self):
|
|
"""IPv6 回环 ::1 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://[::1]/test", purpose="test")
|
|
|
|
def test_ipv6_link_local_rejected(self):
|
|
"""IPv6 链路本地地址应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://[fe80::1]/test", purpose="test")
|
|
|
|
# ── SSRF: 内网主机名 ─────────────────────────────────────────────────
|
|
|
|
def test_localhost_rejected(self):
|
|
"""localhost 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://localhost/test", purpose="test")
|
|
|
|
def test_local_domain_rejected(self):
|
|
""".local 域名应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://printer.local/test", purpose="test")
|
|
|
|
def test_internal_domain_rejected(self):
|
|
""".internal 域名应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http://db.internal/test", purpose="test")
|
|
|
|
# ── URL 格式校验 ─────────────────────────────────────────────────────
|
|
|
|
def test_empty_url_rejected(self):
|
|
"""空 URL 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("", purpose="test")
|
|
|
|
def test_none_url_rejected(self):
|
|
"""None URL 应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety(None, purpose="test") # type: ignore
|
|
|
|
def test_url_too_long_rejected(self):
|
|
"""超长 URL 应该被拒绝."""
|
|
long_url = "https://example.com/" + "a" * 3000
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety(long_url, purpose="test")
|
|
|
|
def test_no_hostname_rejected(self):
|
|
"""缺少主机名应该被拒绝."""
|
|
with self.assertRaises(UrlSecurityError):
|
|
validate_url_safety("http:///test", purpose="test")
|
|
|
|
# ── is_url_safe 便捷函数 ─────────────────────────────────────────────
|
|
|
|
def test_is_url_safe_true(self):
|
|
"""安全 URL 应该返回 True."""
|
|
self.assertTrue(is_url_safe("https://example.com/test", purpose="test"))
|
|
|
|
def test_is_url_safe_false(self):
|
|
"""不安全 URL 应该返回 False."""
|
|
self.assertFalse(is_url_safe("http://127.0.0.1/test", purpose="test"))
|
|
|
|
def test_is_url_safe_empty(self):
|
|
"""空 URL 应该返回 False."""
|
|
self.assertFalse(is_url_safe("", purpose="test"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|