Spaces:
Running
Running
fix: reject noncanonical release archive paths
Browse files- scripts/first_kill.py +4 -1
- scripts/verify_release.py +4 -1
- tests/test_first_kill.py +10 -0
- tests/test_release_archive.py +12 -0
scripts/first_kill.py
CHANGED
|
@@ -132,13 +132,16 @@ def validated_zip_members(bundle: zipfile.ZipFile) -> list[zipfile.ZipInfo]:
|
|
| 132 |
total = 0
|
| 133 |
for member in members:
|
| 134 |
name = member.filename
|
| 135 |
-
|
|
|
|
|
|
|
| 136 |
parts = path.parts
|
| 137 |
normalized = path.as_posix().rstrip("/")
|
| 138 |
if (
|
| 139 |
not name
|
| 140 |
or "\x00" in name
|
| 141 |
or "\\" in name
|
|
|
|
| 142 |
or path.is_absolute()
|
| 143 |
or not normalized
|
| 144 |
or any(part in ("", ".", "..") for part in parts)
|
|
|
|
| 132 |
total = 0
|
| 133 |
for member in members:
|
| 134 |
name = member.filename
|
| 135 |
+
raw_name = name.removesuffix("/")
|
| 136 |
+
raw_parts = raw_name.split("/")
|
| 137 |
+
path = PurePosixPath(raw_name)
|
| 138 |
parts = path.parts
|
| 139 |
normalized = path.as_posix().rstrip("/")
|
| 140 |
if (
|
| 141 |
not name
|
| 142 |
or "\x00" in name
|
| 143 |
or "\\" in name
|
| 144 |
+
or any(part in ("", ".", "..") for part in raw_parts)
|
| 145 |
or path.is_absolute()
|
| 146 |
or not normalized
|
| 147 |
or any(part in ("", ".", "..") for part in parts)
|
scripts/verify_release.py
CHANGED
|
@@ -58,13 +58,16 @@ def validated_members(archive: zipfile.ZipFile) -> list[zipfile.ZipInfo]:
|
|
| 58 |
total = 0
|
| 59 |
for member in members:
|
| 60 |
name = member.filename
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
parts = path.parts
|
| 63 |
normalized = path.as_posix().rstrip("/")
|
| 64 |
if (
|
| 65 |
not name
|
| 66 |
or "\x00" in name
|
| 67 |
or "\\" in name
|
|
|
|
| 68 |
or path.is_absolute()
|
| 69 |
or not normalized
|
| 70 |
or any(part in ("", ".", "..") for part in parts)
|
|
|
|
| 58 |
total = 0
|
| 59 |
for member in members:
|
| 60 |
name = member.filename
|
| 61 |
+
raw_name = name.removesuffix("/")
|
| 62 |
+
raw_parts = raw_name.split("/")
|
| 63 |
+
path = PurePosixPath(raw_name)
|
| 64 |
parts = path.parts
|
| 65 |
normalized = path.as_posix().rstrip("/")
|
| 66 |
if (
|
| 67 |
not name
|
| 68 |
or "\x00" in name
|
| 69 |
or "\\" in name
|
| 70 |
+
or any(part in ("", ".", "..") for part in raw_parts)
|
| 71 |
or path.is_absolute()
|
| 72 |
or not normalized
|
| 73 |
or any(part in ("", ".", "..") for part in parts)
|
tests/test_first_kill.py
CHANGED
|
@@ -254,6 +254,16 @@ class FirstKillTests(unittest.TestCase):
|
|
| 254 |
with self.assertRaisesRegex(first_kill.FirstKillError, "unsafe path"):
|
| 255 |
first_kill.safe_extract(archive, root / "out")
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
def test_safe_extract_rejects_duplicate_paths(self) -> None:
|
| 258 |
with tempfile.TemporaryDirectory() as raw:
|
| 259 |
root = Path(raw)
|
|
|
|
| 254 |
with self.assertRaisesRegex(first_kill.FirstKillError, "unsafe path"):
|
| 255 |
first_kill.safe_extract(archive, root / "out")
|
| 256 |
|
| 257 |
+
def test_safe_extract_rejects_noncanonical_components(self) -> None:
|
| 258 |
+
for member_name in ("release/./install.py", "release//install.py"):
|
| 259 |
+
with self.subTest(member_name=member_name), tempfile.TemporaryDirectory() as raw:
|
| 260 |
+
root = Path(raw)
|
| 261 |
+
archive = root / "noncanonical.zip"
|
| 262 |
+
with zipfile.ZipFile(archive, "w") as bundle:
|
| 263 |
+
bundle.writestr(member_name, "untrusted")
|
| 264 |
+
with self.assertRaisesRegex(first_kill.FirstKillError, "unsafe path"):
|
| 265 |
+
first_kill.safe_extract(archive, root / "out")
|
| 266 |
+
|
| 267 |
def test_safe_extract_rejects_duplicate_paths(self) -> None:
|
| 268 |
with tempfile.TemporaryDirectory() as raw:
|
| 269 |
root = Path(raw)
|
tests/test_release_archive.py
CHANGED
|
@@ -66,6 +66,18 @@ class ReleaseArchiveTests(unittest.TestCase):
|
|
| 66 |
):
|
| 67 |
verify_release.validated_members(archive)
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
def test_verifier_rejects_special_members(self) -> None:
|
| 70 |
with tempfile.TemporaryDirectory() as raw:
|
| 71 |
archive_path = Path(raw) / "special.zip"
|
|
|
|
| 66 |
):
|
| 67 |
verify_release.validated_members(archive)
|
| 68 |
|
| 69 |
+
def test_verifier_rejects_noncanonical_components(self) -> None:
|
| 70 |
+
for member_name in ("release/./install.py", "release//install.py"):
|
| 71 |
+
with self.subTest(member_name=member_name), tempfile.TemporaryDirectory() as raw:
|
| 72 |
+
archive_path = Path(raw) / "noncanonical.zip"
|
| 73 |
+
with zipfile.ZipFile(archive_path, "w") as archive:
|
| 74 |
+
archive.writestr(member_name, "untrusted")
|
| 75 |
+
with (
|
| 76 |
+
zipfile.ZipFile(archive_path) as archive,
|
| 77 |
+
self.assertRaisesRegex(SystemExit, "unsafe path"),
|
| 78 |
+
):
|
| 79 |
+
verify_release.validated_members(archive)
|
| 80 |
+
|
| 81 |
def test_verifier_rejects_special_members(self) -> None:
|
| 82 |
with tempfile.TemporaryDirectory() as raw:
|
| 83 |
archive_path = Path(raw) / "special.zip"
|