mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-07 15:51:28 +08:00
fix(security): release SSRF DNS inflight lock outside async with block (#5834)
This commit is contained in:
@@ -550,3 +550,102 @@ class SecurityUtilsTest(TestCase):
|
||||
self.assertEqual(errors, [])
|
||||
self.assertTrue(all(results))
|
||||
self.assertEqual(len(results), 8 * 50)
|
||||
|
||||
def test_async_dns_resolution_failure_releases_inflight_lock(self):
|
||||
"""
|
||||
DNS 解析失败后 in-flight 锁字典中必须被清理,避免每个解析失败的 hostname
|
||||
都在 `_dns_inflight_locks` 里残留一把 `asyncio.Lock`。
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
async def fail_getaddrinfo(*_args, **_kwargs):
|
||||
raise socket.gaierror()
|
||||
|
||||
async def run() -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
with patch.object(loop, "getaddrinfo", side_effect=fail_getaddrinfo):
|
||||
result = await SecurityUtils._hostname_addresses_async(
|
||||
"bad-host.example"
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
asyncio.run(run())
|
||||
self.assertNotIn(
|
||||
"bad-host.example",
|
||||
_dns_inflight_locks,
|
||||
"解析失败路径必须释放 in-flight 锁字典条目",
|
||||
)
|
||||
|
||||
def test_async_dns_resolution_success_releases_inflight_lock(self):
|
||||
"""
|
||||
正常解析完成后 in-flight 锁字典也必须被清理,避免 hostname 累积。
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
async def fake_getaddrinfo(*_args, **_kwargs):
|
||||
return [
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
0,
|
||||
"",
|
||||
("93.184.216.34", 0),
|
||||
)
|
||||
]
|
||||
|
||||
async def run() -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
with patch.object(loop, "getaddrinfo", side_effect=fake_getaddrinfo):
|
||||
result = await SecurityUtils._hostname_addresses_async(
|
||||
"ok-host.example"
|
||||
)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
asyncio.run(run())
|
||||
self.assertNotIn(
|
||||
"ok-host.example",
|
||||
_dns_inflight_locks,
|
||||
"正常解析路径必须释放 in-flight 锁字典条目",
|
||||
)
|
||||
|
||||
def test_async_dns_concurrent_waiters_release_inflight_lock(self):
|
||||
"""
|
||||
并发未命中场景下,所有等待者完成后 in-flight 锁字典也必须被清理,
|
||||
覆盖"等到锁但缓存已被前一个协程回填"的二次返回路径。
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
async def run() -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
release = asyncio.Event()
|
||||
|
||||
async def slow_getaddrinfo(*_args, **_kwargs):
|
||||
await release.wait()
|
||||
return [
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
0,
|
||||
"",
|
||||
("93.184.216.34", 0),
|
||||
)
|
||||
]
|
||||
|
||||
with patch.object(loop, "getaddrinfo", side_effect=slow_getaddrinfo):
|
||||
tasks = [
|
||||
asyncio.create_task(
|
||||
SecurityUtils._hostname_addresses_async("multi-host.example")
|
||||
)
|
||||
for _ in range(5)
|
||||
]
|
||||
await asyncio.sleep(0)
|
||||
await asyncio.sleep(0)
|
||||
release.set()
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
asyncio.run(run())
|
||||
self.assertNotIn(
|
||||
"multi-host.example",
|
||||
_dns_inflight_locks,
|
||||
"并发等待者全部退出后必须释放 in-flight 锁字典条目",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user