From 911478495a9745e1c7999aa5e948f25add7fbc03 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 22 Jul 2026 14:24:32 +0800 Subject: [PATCH 1/5] =?UTF-8?q?test(p3-1):=20=E7=AC=AC=E5=8D=81=E6=B3=A2?= =?UTF-8?q?=20-=20url=5Fsecurity=20=E5=AE=89=E5=85=A8=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=2072=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖 url_security.py 核心安全逻辑: - validate_url_safety: scheme/端口/主机名/URL长度 等基础校验 - SSRF防护: 内网IP/回环/链路本地/组播/未指定/保留地址 - 内网主机名防护: localhost/.local/.internal/metadata - 可信域名白名单: 精确匹配+子域名匹配 - is_url_safe 便捷函数 - 文件魔数校验: PNG/JPEG/GIF/WEBP/BMP/WAV/MP3/OGG/FLAC - safe_download_file / safe_download_bytes (mock网络) - 大小限制 / MIME类型白名单 / Content-Length预检 --- tests/unit/test_url_security.py | 804 ++++++++++++++++++++++---------- 1 file changed, 549 insertions(+), 255 deletions(-) diff --git a/tests/unit/test_url_security.py b/tests/unit/test_url_security.py index 63da47669..e0a01d6ad 100755 --- a/tests/unit/test_url_security.py +++ b/tests/unit/test_url_security.py @@ -1,296 +1,590 @@ -"""URL 安全校验工具单元测试 — SSRF 防护.""" +""" +url_security URL安全校验单元测试 -from __future__ import annotations +覆盖: +- validate_url_safety: scheme/主机/端口/SSRF/内网域名/白名单 +- is_url_safe: 便捷函数 +- UrlSecurityError / NoRedirectHandler +- _validate_magic_number: 文件魔数校验 +- safe_download_file / safe_download_bytes: mock 网络测试 +""" import os -import sys -import unittest - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "apps", "worker")) - -import shutil import tempfile -from video_processing.url_security import ( # noqa: E402 - ALLOWED_AUDIO_MIME_TYPES, +import pytest +from unittest.mock import patch, MagicMock + +from packages.shared.url_security import ( + ALLOWED_SCHEMES, + ALLOWED_PORTS, + MAX_URL_LENGTH, UrlSecurityError, - is_url_safe, - safe_download_bytes, - safe_download_file, + NoRedirectHandler, validate_url_safety, + is_url_safe, + _validate_magic_number, + _check_internal_hostnames, + _is_trusted_domain, + safe_download_file, + safe_download_bytes, + ALLOWED_AUDIO_MIME_TYPES, + ALLOWED_IMAGE_MIME_TYPES, ) -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")) +# ── validate_url_safety 基础校验 ───────────────────────────────────────────── -if __name__ == "__main__": - unittest.main() +class TestValidateUrlSafetyBasics: + """URL 安全校验基础测试""" + + def test_valid_http_url(self): + url = "http://example.com/file.mp4" + result = validate_url_safety(url) + assert result == url + + def test_valid_https_url(self): + url = "https://example.com/file.mp4" + result = validate_url_safety(url) + assert result == url + + def test_empty_url_raises(self): + with pytest.raises(UrlSecurityError, match="为空"): + validate_url_safety("") + + def test_none_url_raises(self): + with pytest.raises(UrlSecurityError): + validate_url_safety(None) + + def test_url_too_long_raises(self): + long_url = "https://example.com/" + "a" * 2050 + with pytest.raises(UrlSecurityError, match="过长"): + validate_url_safety(long_url) + + def test_url_at_max_length_ok(self): + base = "https://example.com/" + pad = "a" * (MAX_URL_LENGTH - len(base)) + url = base + pad + assert len(url) <= MAX_URL_LENGTH + result = validate_url_safety(url) + assert result == url + + def test_invalid_scheme_ftp_raises(self): + with pytest.raises(UrlSecurityError, match="scheme"): + validate_url_safety("ftp://example.com/file") + + def test_invalid_scheme_file_raises(self): + with pytest.raises(UrlSecurityError, match="scheme"): + validate_url_safety("file:///etc/passwd") + + def test_invalid_scheme_data_raises(self): + with pytest.raises(UrlSecurityError, match="scheme"): + validate_url_safety("data:text/html,