Spaces:
Running
Running
Test SSRF and upload filename hardening
Browse files- tests/test_downloader.py +54 -0
tests/test_downloader.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from unittest.mock import AsyncMock
|
| 2 |
|
|
|
|
| 3 |
import respx
|
| 4 |
from httpx import Response
|
| 5 |
|
|
|
|
| 6 |
from app.services.downloader import Downloader
|
| 7 |
from app.services.validator import MediaValidator
|
| 8 |
|
|
@@ -18,3 +23,52 @@ async def test_url_download_streams_to_disk(settings, tmp_path) -> None:
|
|
| 18 |
path, mime_type = await downloader.download(url, tmp_path)
|
| 19 |
assert path.read_bytes() == b"ID3data"
|
| 20 |
assert mime_type == "audio/mpeg"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import socket
|
| 4 |
from unittest.mock import AsyncMock
|
| 5 |
|
| 6 |
+
import pytest
|
| 7 |
import respx
|
| 8 |
from httpx import Response
|
| 9 |
|
| 10 |
+
from app.core.exceptions import InputError
|
| 11 |
from app.services.downloader import Downloader
|
| 12 |
from app.services.validator import MediaValidator
|
| 13 |
|
|
|
|
| 23 |
path, mime_type = await downloader.download(url, tmp_path)
|
| 24 |
assert path.read_bytes() == b"ID3data"
|
| 25 |
assert mime_type == "audio/mpeg"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
async def test_validate_url_rejects_non_global_dns_answer(settings, monkeypatch) -> None:
|
| 29 |
+
settings.allow_private_urls = False
|
| 30 |
+
downloader = Downloader(settings, MediaValidator(settings))
|
| 31 |
+
|
| 32 |
+
def private_answer(*args, **kwargs):
|
| 33 |
+
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", 443))]
|
| 34 |
+
|
| 35 |
+
monkeypatch.setattr(socket, "getaddrinfo", private_answer)
|
| 36 |
+
with pytest.raises(InputError, match="Private, loopback"):
|
| 37 |
+
await downloader.validate_url("https://example.test/media.mp4")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
async def test_validate_url_rejects_mixed_public_and_private_dns(settings, monkeypatch) -> None:
|
| 41 |
+
settings.allow_private_urls = False
|
| 42 |
+
downloader = Downloader(settings, MediaValidator(settings))
|
| 43 |
+
|
| 44 |
+
def mixed_answers(*args, **kwargs):
|
| 45 |
+
return [
|
| 46 |
+
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 443)),
|
| 47 |
+
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("10.0.0.4", 443)),
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
monkeypatch.setattr(socket, "getaddrinfo", mixed_answers)
|
| 51 |
+
with pytest.raises(InputError, match="Private, loopback"):
|
| 52 |
+
await downloader.validate_url("https://example.test/media.mp4")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@pytest.mark.parametrize("url", [
|
| 56 |
+
"file:///etc/passwd",
|
| 57 |
+
"https://user:password@example.test/media.mp4",
|
| 58 |
+
"https://example.test:invalid/media.mp4",
|
| 59 |
+
"https://example.test:99999/media.mp4",
|
| 60 |
+
])
|
| 61 |
+
async def test_validate_url_rejects_invalid_or_credentialed_urls(settings, url: str) -> None:
|
| 62 |
+
settings.allow_private_urls = False
|
| 63 |
+
downloader = Downloader(settings, MediaValidator(settings))
|
| 64 |
+
with pytest.raises(InputError):
|
| 65 |
+
await downloader.validate_url(url)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def test_safe_filename_normalizes_windows_paths_and_rejects_header_injection(settings) -> None:
|
| 69 |
+
validator = MediaValidator(settings)
|
| 70 |
+
assert validator.safe_filename(r"C:\\temp\\clip.mp4") == "clip.mp4"
|
| 71 |
+
with pytest.raises(InputError):
|
| 72 |
+
validator.safe_filename("clip.mp4\r\nX-Evil: yes")
|
| 73 |
+
with pytest.raises(InputError):
|
| 74 |
+
validator.safe_filename('clip".mp4')
|