unzip-http 0.7__tar.gz → 0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unzip-http
3
- Version: 0.7
3
+ Version: 0.8
4
4
  Summary: extract files from .zip files over http without downloading entire archive
5
5
  Author: Saul Pwanson
6
6
  Project-URL: Homepage, https://github.com/saulpw/unzip-http
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unzip-http"
7
- version = "0.7"
7
+ version = "0.8"
8
8
  description = "extract files from .zip files over http without downloading entire archive"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,37 @@
1
+ import io
2
+ import pathlib
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+
7
+ import unzip_http
8
+
9
+
10
+ def fake_rzf(contents):
11
+ rzf = unzip_http.RemoteZipFile.__new__(unzip_http.RemoteZipFile)
12
+ rzf.open = lambda fn: io.BytesIO(contents)
13
+ return rzf
14
+
15
+
16
+ @pytest.mark.parametrize('member', ['../escaped.txt', '/tmp/absolute.txt', 'sub/../../escaped.txt'])
17
+ def test_extract_refuses_escaping_member(tmp_path, member):
18
+ target = tmp_path/'target'
19
+ target.mkdir()
20
+ with pytest.raises(Exception, match='unsafe path'):
21
+ fake_rzf(b'x').extract(member, path=target)
22
+ assert list(tmp_path.rglob('*')) == [target]
23
+
24
+
25
+ def test_extract_writes_inside_target(tmp_path):
26
+ fake_rzf(b'hello').extract('sub/ok.txt', path=tmp_path)
27
+ assert (tmp_path/'sub'/'ok.txt').read_bytes() == b'hello'
28
+
29
+
30
+ def test_cli_full_filepaths_refuses_escaping_member(tmp_path, monkeypatch):
31
+ monkeypatch.chdir(tmp_path)
32
+ monkeypatch.setattr(unzip_http, 'extract_one', lambda outfile, rzf, f, ofname: outfile.write(b'x'))
33
+ f = SimpleNamespace(filename='../escaped.txt', compress_size=1)
34
+ args = SimpleNamespace(files=['*'], stdout=False, full_filepaths=True)
35
+ with pytest.raises(Exception, match='unsafe path'):
36
+ unzip_http.download_file(f, None, args)
37
+ assert not (tmp_path.parent/'escaped.txt').exists()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unzip-http
3
- Version: 0.7
3
+ Version: 0.8
4
4
  Summary: extract files from .zip files over http without downloading entire archive
5
5
  Author: Saul Pwanson
6
6
  Project-URL: Homepage, https://github.com/saulpw/unzip-http
@@ -3,6 +3,7 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  unzip_http.py
6
+ tests/test_unzip_http.py
6
7
  unzip_http.egg-info/PKG-INFO
7
8
  unzip_http.egg-info/SOURCES.txt
8
9
  unzip_http.egg-info/dependency_links.txt
@@ -56,7 +56,7 @@ import pathlib
56
56
  import urllib.parse
57
57
 
58
58
 
59
- __version__ = '0.6'
59
+ __version__ = '0.8'
60
60
 
61
61
 
62
62
  def error(s):
@@ -117,6 +117,14 @@ class RemoteZipInfo:
117
117
  i += fieldsz
118
118
 
119
119
 
120
+ def safe_extract_path(base, member):
121
+ base = pathlib.Path(base).resolve()
122
+ target = (base / member).resolve()
123
+ if base != target and base not in target.parents:
124
+ raise Exception(f'refusing unsafe path in zip member: {member!r}')
125
+ return target
126
+
127
+
120
128
  class RemoteZipFile:
121
129
  fmt_eocd = '<IHHHHIIH' # end of central directory
122
130
  fmt_eocd64 = '<IQHHIIQQQQ' # end of central directory ZIP64
@@ -223,10 +231,10 @@ class RemoteZipFile:
223
231
 
224
232
  path = path or pathlib.Path('.')
225
233
 
226
- outpath = path/member
234
+ outpath = safe_extract_path(path, member)
227
235
  os.makedirs(outpath.parent, exist_ok=True)
228
236
  with self.open(member) as fpin:
229
- with open(path/member, mode='wb') as fpout:
237
+ with open(outpath, mode='wb') as fpout:
230
238
  while True:
231
239
  r = fpin.read(65536)
232
240
  if not r:
@@ -354,6 +362,7 @@ def download_file(f, rzf, args):
354
362
  else:
355
363
  path = pathlib.Path(f.filename)
356
364
  if args.full_filepaths:
365
+ path = safe_extract_path('.', path)
357
366
  path.parent.mkdir(parents=True, exist_ok=True)
358
367
  else:
359
368
  path = path.name
File without changes
File without changes
File without changes
File without changes