mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-03 14:39:56 +08:00
fix(system): allow configured image proxy private ranges (#5831)
This commit is contained in:
@@ -162,3 +162,113 @@ class SecurityUtilsTest(TestCase):
|
||||
block_private=True,
|
||||
)
|
||||
)
|
||||
|
||||
def test_is_safe_url_allows_configured_private_range_after_domain_match(self):
|
||||
"""
|
||||
图片域名命中 allowlist 后,可通过配置允许 TUN fake-ip 等特定非公网网段。
|
||||
"""
|
||||
with patch(
|
||||
"app.utils.security.socket.getaddrinfo",
|
||||
return_value=[
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
0,
|
||||
"",
|
||||
("198.18.16.96", 0),
|
||||
)
|
||||
],
|
||||
), patch("app.utils.security.logger.debug") as debug_log:
|
||||
self.assertTrue(
|
||||
SecurityUtils.is_safe_url(
|
||||
"https://img1.doubanio.com/poster.webp",
|
||||
{"doubanio.com"},
|
||||
block_private=True,
|
||||
allowed_private_ranges=["198.18.0.0/15"],
|
||||
)
|
||||
)
|
||||
debug_message = debug_log.call_args.args[0]
|
||||
self.assertIn("ips=198.18.16.96", debug_message)
|
||||
self.assertIn("ranges=198.18.0.0/15", debug_message)
|
||||
|
||||
def test_is_safe_url_blocks_configured_private_range_without_domain_match(self):
|
||||
"""
|
||||
非公网网段例外必须依附域名白名单,不能单独放行任意用户 URL。
|
||||
"""
|
||||
with patch(
|
||||
"app.utils.security.socket.getaddrinfo",
|
||||
return_value=[
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
0,
|
||||
"",
|
||||
("198.18.16.96", 0),
|
||||
)
|
||||
],
|
||||
):
|
||||
self.assertFalse(
|
||||
SecurityUtils.is_safe_url(
|
||||
"https://attacker.example.com/poster.webp",
|
||||
{"doubanio.com"},
|
||||
block_private=True,
|
||||
allowed_private_ranges=["198.18.0.0/15"],
|
||||
)
|
||||
)
|
||||
|
||||
def test_is_safe_url_blocks_private_result_outside_configured_range(self):
|
||||
"""
|
||||
仅允许显式配置的非公网网段,其它内网解析结果仍按 SSRF 风险拦截。
|
||||
"""
|
||||
with patch(
|
||||
"app.utils.security.socket.getaddrinfo",
|
||||
return_value=[
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
0,
|
||||
"",
|
||||
("10.0.0.8", 0),
|
||||
)
|
||||
],
|
||||
):
|
||||
self.assertFalse(
|
||||
SecurityUtils.is_safe_url(
|
||||
"https://assets.example.com/poster.jpg",
|
||||
{"example.com"},
|
||||
block_private=True,
|
||||
allowed_private_ranges=["198.18.0.0/15"],
|
||||
)
|
||||
)
|
||||
|
||||
def test_is_safe_url_blocks_mixed_allowed_and_disallowed_private_results(self):
|
||||
"""
|
||||
同一域名的解析结果必须全部落在允许网段内,避免部分安全结果掩盖风险地址。
|
||||
"""
|
||||
with patch(
|
||||
"app.utils.security.socket.getaddrinfo",
|
||||
return_value=[
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
0,
|
||||
"",
|
||||
("198.18.16.96", 0),
|
||||
),
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
0,
|
||||
"",
|
||||
("10.0.0.8", 0),
|
||||
),
|
||||
],
|
||||
):
|
||||
self.assertFalse(
|
||||
SecurityUtils.is_safe_url(
|
||||
"https://assets.example.com/poster.jpg",
|
||||
{"example.com"},
|
||||
block_private=True,
|
||||
allowed_private_ranges=["198.18.0.0/15"],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import sys
|
||||
import unittest
|
||||
from types import ModuleType, SimpleNamespace
|
||||
@@ -134,6 +135,47 @@ class NettestSecurityTest(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(resp)
|
||||
|
||||
def test_fetch_image_allows_configured_private_range_after_domain_match(self):
|
||||
"""
|
||||
图片代理在域名白名单命中后,可按配置放行指定非公网解析网段。
|
||||
"""
|
||||
image_helper = Mock()
|
||||
image_helper.async_fetch_image = AsyncMock(return_value=b"image-bytes")
|
||||
|
||||
with patch.object(system_endpoint, "ImageHelper", return_value=image_helper), patch.object(
|
||||
system_endpoint.HashUtils, "md5", return_value="etag", create=True
|
||||
), patch.object(
|
||||
system_endpoint.RequestUtils, "generate_cache_headers", return_value={}, create=True
|
||||
), patch.object(
|
||||
system_endpoint.SecurityUtils,
|
||||
"_is_global_hostname",
|
||||
return_value=False,
|
||||
), patch.object(
|
||||
system_endpoint.SecurityUtils,
|
||||
"_hostname_addresses",
|
||||
return_value=[ipaddress.ip_address("198.18.16.96")],
|
||||
), patch.object(
|
||||
system_endpoint.settings,
|
||||
"IMAGE_PROXY_ALLOWED_PRIVATE_RANGES",
|
||||
["198.18.0.0/15"],
|
||||
), patch(
|
||||
"app.utils.security.logger.debug",
|
||||
):
|
||||
resp = asyncio.run(
|
||||
system_endpoint.fetch_image(
|
||||
url="https://img1.doubanio.com/poster.webp",
|
||||
allowed_domains={"doubanio.com"},
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
image_helper.async_fetch_image.assert_awaited_once_with(
|
||||
url="https://img1.doubanio.com/poster.webp",
|
||||
proxy=None,
|
||||
use_cache=False,
|
||||
cookies=None,
|
||||
)
|
||||
|
||||
def test_fetch_image_blocks_tampered_signed_private_url(self):
|
||||
"""
|
||||
私网签名绑定完整 URL,改动路径后不能继续代理。
|
||||
|
||||
Reference in New Issue
Block a user