noqt commited on
Commit
a67aa64
·
1 Parent(s): d4c00c5

fix: reject noncanonical release archive paths

Browse files
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
- path = PurePosixPath(name)
 
 
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
- path = PurePosixPath(name)
 
 
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"