blob: d959a815520a6960e8f427f1649e8e15b341c136 [file]
#!/usr/bin/env python3
import pathlib
import tempfile
import unittest
from registry import (
ALLOWED_DOWNLOAD_SCHEMES,
RegistryClient,
RegistryException,
_validate_download_url,
)
class TestRegistryClient(unittest.TestCase):
def setUp(self):
self.tmp_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp_dir.cleanup)
self.registry_root = pathlib.Path(self.tmp_dir.name)
self.registry = RegistryClient(self.registry_root)
def test_get_patch_file_path_allows_relative_path(self):
patch_file = self.registry.get_patch_file_path("foo", "1.0.0", "fix.patch")
self.assertEqual(
patch_file,
self.registry_root.resolve() / "modules" / "foo" / "1.0.0" / "patches" / "fix.patch",
)
def test_get_patch_file_path_allows_nested_relative_path(self):
patch_file = self.registry.get_patch_file_path("foo", "1.0.0", "subdir/fix.patch")
self.assertEqual(
patch_file,
self.registry_root.resolve() / "modules" / "foo" / "1.0.0" / "patches" / "subdir" / "fix.patch",
)
def test_get_patch_file_path_rejects_absolute_path(self):
patch_file = pathlib.Path(self.tmp_dir.name) / "outside.patch"
with self.assertRaisesRegex(RegistryException, "must point inside"):
self.registry.get_patch_file_path("foo", "1.0.0", str(patch_file))
def test_get_patch_file_path_rejects_parent_traversal(self):
with self.assertRaisesRegex(RegistryException, "must point inside"):
self.registry.get_patch_file_path("foo", "1.0.0", "../outside.patch")
class TestValidateDownloadUrl(unittest.TestCase):
"""`_validate_download_url` is the choke point that prevents PR-supplied
URLs (`source.url`, `mirror_urls`, patch URLs) from triggering local-file
reads or non-HTTP protocol abuse via `urllib.request.urlopen`.
"""
def test_https_is_allowed(self):
parts = _validate_download_url("https://example.com/archive.tar.gz")
self.assertEqual(parts.scheme, "https")
def test_http_is_allowed(self):
# HTTP is intentionally permitted because legacy `mirror_urls` exist.
parts = _validate_download_url("http://mirrors.example.com/x.tar.gz")
self.assertEqual(parts.scheme, "http")
def test_file_scheme_is_rejected(self):
# The motivating case: a PR-submitted `source.json` whose `url` reads
# local files from the validation runner.
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("file:///etc/passwd")
def test_proc_environ_file_scheme_is_rejected(self):
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("file:///proc/self/environ")
def test_dev_null_file_url_is_allowed(self):
# `file:///dev/null` is an allow-listed benign exception used by
# `tools/update_integrity_test.sh`. It always reads as empty bytes
# so it has no exfiltration value (see comment on `_BENIGN_FILE_URLS`
# in `registry.py`).
parts = _validate_download_url("file:///dev/null")
self.assertEqual(parts.scheme, "file")
self.assertEqual(parts.path, "/dev/null")
def test_other_file_url_is_rejected_even_with_similar_path(self):
# Make sure the allow-list is exact match, not a prefix/substring
# match. `file:///dev/null/../etc/passwd` MUST still be rejected.
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("file:///dev/null/../etc/passwd")
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("file:///dev/nullx")
def test_ftp_scheme_is_rejected(self):
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("ftp://example.com/archive.tar.gz")
def test_gopher_scheme_is_rejected(self):
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("gopher://example.com:70/_test")
def test_data_scheme_is_rejected(self):
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("data:text/plain;base64,SGVsbG8=")
def test_jar_scheme_is_rejected(self):
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("jar:file:///tmp/x.jar!/inner")
def test_empty_scheme_is_rejected(self):
# `urllib.parse.urlsplit` returns scheme="" for raw paths like
# "/etc/passwd" or "example.com/archive.tar.gz".
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("/etc/passwd")
with self.assertRaisesRegex(RegistryException, "not allowed"):
_validate_download_url("example.com/archive.tar.gz")
def test_uppercase_scheme_is_normalised(self):
# `urlsplit` lowercases the scheme; both forms map to "https".
parts = _validate_download_url("HTTPS://example.com/x.tar.gz")
self.assertEqual(parts.scheme, "https")
def test_allowlist_is_minimal(self):
# If this test fails, audit the new scheme: it MUST be safe to
# invoke `urllib.request.urlopen` on attacker-controlled URLs of
# this scheme. See the comment on `ALLOWED_DOWNLOAD_SCHEMES`.
self.assertEqual(ALLOWED_DOWNLOAD_SCHEMES, frozenset({"http", "https"}))
if __name__ == "__main__":
unittest.main()