zipcheck 0.2.0__tar.gz → 0.3.0__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.
- {zipcheck-0.2.0 → zipcheck-0.3.0}/CHANGELOG.md +24 -1
- {zipcheck-0.2.0 → zipcheck-0.3.0}/Dockerfile +1 -1
- {zipcheck-0.2.0 → zipcheck-0.3.0}/PKG-INFO +108 -30
- {zipcheck-0.2.0 → zipcheck-0.3.0}/README.md +104 -29
- {zipcheck-0.2.0 → zipcheck-0.3.0}/SECURITY.md +14 -6
- {zipcheck-0.2.0 → zipcheck-0.3.0}/docs/production.md +15 -7
- {zipcheck-0.2.0 → zipcheck-0.3.0}/packaging/aur/PKGBUILD +2 -2
- {zipcheck-0.2.0 → zipcheck-0.3.0}/pyproject.toml +4 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/__init__.py +1 -1
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/cli.py +56 -1
- zipcheck-0.3.0/src/zipcheck/decrypt.py +169 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/engine.py +249 -142
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/zipfmt.py +4 -1
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/limits.py +6 -1
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/verify.py +126 -56
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck.egg-info/PKG-INFO +108 -30
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck.egg-info/SOURCES.txt +2 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck.egg-info/requires.txt +4 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/helpers.py +18 -0
- zipcheck-0.3.0/tests/test_password.py +196 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/.dockerignore +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/CONTRIBUTING.md +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/LICENSE +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/MANIFEST.in +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/docs/metadata-map.md +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/fuzz/fuzz_scan.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/fuzz/generate_seeds.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/packaging/aur/.SRCINFO +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/packaging/aur/refresh.sh +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/setup.cfg +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/__main__.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/detect.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/__init__.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/bzip2fmt.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/gzipfmt.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/probe.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/rarfmt.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/sevenzipfmt.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/tarfmt.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/xzfmt.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/formats/zstdfmt.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/model.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/py.typed +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck/reader.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck.egg-info/dependency_links.txt +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck.egg-info/entry_points.txt +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/src/zipcheck.egg-info/top_level.txt +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/__init__.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/conftest.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/test_cli.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/test_other_formats.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/test_property.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/test_robustness.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/test_tar.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/test_verify.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tests/test_zip.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tools/benchmark.py +0 -0
- {zipcheck-0.2.0 → zipcheck-0.3.0}/tools/generate_samples.py +0 -0
|
@@ -6,6 +6,28 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-10-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Password support for encrypted ZIPs: `--password`, `--password-file` and
|
|
14
|
+
`--ask-password` decrypt WinZip AES and legacy ZipCrypto members during the
|
|
15
|
+
scan, so nested encrypted bombs are followed and reported instead of landing
|
|
16
|
+
in `unknown`.
|
|
17
|
+
- Password-aware content verification: encrypted ZIP members are decrypted into
|
|
18
|
+
the counting sink and participate in the understated/overstated and
|
|
19
|
+
`VERIFIED_EXPANSION` checks.
|
|
20
|
+
- Optional `aes` extra (`pyzipper`) for AES decryption; ZipCrypto works with the
|
|
21
|
+
standard library alone. The bundled Docker image installs the extra, and the
|
|
22
|
+
AUR package lists `python-pyzipper` as an optional dependency.
|
|
23
|
+
|
|
24
|
+
### Security
|
|
25
|
+
|
|
26
|
+
- Passwords are kept out of `Limits` representations, findings, notes and JSON
|
|
27
|
+
output.
|
|
28
|
+
- A wrong password, a missing AES backend, or an unsupported encryption method
|
|
29
|
+
fails closed: `unknown` in static mode, unverified under `--production`.
|
|
30
|
+
|
|
9
31
|
## [0.2.0] - 2026-10-24
|
|
10
32
|
|
|
11
33
|
### Added
|
|
@@ -62,7 +84,8 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
62
84
|
- A 7-level nested chain no longer scans `clean` when the depth limit truncates
|
|
63
85
|
inspection.
|
|
64
86
|
|
|
65
|
-
[Unreleased]: https://github.com/spaxe-dev/zipcheck/compare/v0.
|
|
87
|
+
[Unreleased]: https://github.com/spaxe-dev/zipcheck/compare/v0.3.0...HEAD
|
|
88
|
+
[0.3.0]: https://github.com/spaxe-dev/zipcheck/releases/tag/v0.3.0
|
|
66
89
|
[0.2.0]: https://github.com/spaxe-dev/zipcheck/releases/tag/v0.2.0
|
|
67
90
|
|
|
68
91
|
## [0.1.0] - 2026-10-24
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zipcheck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Static zip-bomb and archive-bomb detector that never extracts anything
|
|
5
5
|
Author: zipcheck contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,9 +29,12 @@ Classifier: Typing :: Typed
|
|
|
29
29
|
Requires-Python: >=3.10
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
License-File: LICENSE
|
|
32
|
+
Provides-Extra: aes
|
|
33
|
+
Requires-Dist: pyzipper>=0.3.6; extra == "aes"
|
|
32
34
|
Provides-Extra: dev
|
|
33
35
|
Requires-Dist: hypothesis>=6; extra == "dev"
|
|
34
36
|
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
37
|
+
Requires-Dist: pyzipper>=0.3.6; extra == "dev"
|
|
35
38
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
36
39
|
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
37
40
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
@@ -84,6 +87,7 @@ $ zipcheck suspicious.zip
|
|
|
84
87
|
- [Usage](#usage)
|
|
85
88
|
- [Detection rules](#detection-rules)
|
|
86
89
|
- [Content verification](#content-verification)
|
|
90
|
+
- [Encrypted ZIPs](#encrypted-zips)
|
|
87
91
|
- [How each format is measured](#how-each-format-is-measured)
|
|
88
92
|
- [Nested archives and deep bombs](#nested-archives-and-deep-bombs)
|
|
89
93
|
- [Safety model](#safety-model)
|
|
@@ -140,10 +144,11 @@ examples.
|
|
|
140
144
|
## What it detects
|
|
141
145
|
|
|
142
146
|
- **ZIP / ZIP64** — declared expansion, per-entry impossible ratios, overlapping
|
|
143
|
-
or shared entries, encrypted archive members
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
compressed
|
|
147
|
+
or shared entries, encrypted archive members (decrypted and followed when a
|
|
148
|
+
password is supplied), member count, contradictory local/central headers,
|
|
149
|
+
entries hidden from the EOCD count, and a bounded stream-tail probe that
|
|
150
|
+
catches a compressed stream extending past a forged compressed size when it
|
|
151
|
+
leaves an unexplained gap.
|
|
147
152
|
- **TAR** — huge declared members, sparse members, truncated archives, PAX and
|
|
148
153
|
GNU long names, nested archives.
|
|
149
154
|
- **gzip** — every stream is measured with a bounded multi-member probe, so
|
|
@@ -159,11 +164,12 @@ examples.
|
|
|
159
164
|
unknown.
|
|
160
165
|
- **Nested archives** — ZIP, TAR, gzip, bzip2 and xz are followed up to a
|
|
161
166
|
configurable depth. Archive-like members inside 7z/RAR, encrypted archive-like
|
|
162
|
-
members, and chains stopped by a cap are reported `unknown`
|
|
163
|
-
silently skipped.
|
|
167
|
+
members without a password, and chains stopped by a cap are reported `unknown`
|
|
168
|
+
instead of being silently skipped.
|
|
164
169
|
- **Content verification** — optional bounded decompression to a counting sink
|
|
165
170
|
catches headers that understate the real size (ZIP stored/deflate/bzip2/LZMA,
|
|
166
|
-
gzip, bzip2, xz/LZMA; TAR plan validation)
|
|
171
|
+
gzip, bzip2, xz/LZMA; TAR plan validation), including encrypted ZIP members
|
|
172
|
+
when a password is provided.
|
|
167
173
|
|
|
168
174
|
Every result is one of: `clean`, `suspicious`, `dangerous`, `unknown`.
|
|
169
175
|
**`unknown` is not a safe verdict** — it means the scanner could not prove
|
|
@@ -179,17 +185,23 @@ anything either way.
|
|
|
179
185
|
|
|
180
186
|
## Install
|
|
181
187
|
|
|
182
|
-
Requires Python 3.10+.
|
|
188
|
+
Requires Python 3.10+. There are no required runtime dependencies.
|
|
189
|
+
|
|
190
|
+
WinZip AES-encrypted ZIPs need the optional `aes` extra, which installs
|
|
191
|
+
[`pyzipper`](https://pypi.org/project/pyzipper/). Legacy ZipCrypto archives work
|
|
192
|
+
without it.
|
|
183
193
|
|
|
184
194
|
```bash
|
|
185
195
|
pip install zipcheck
|
|
196
|
+
pip install 'zipcheck[aes]' # + AES-encrypted ZIP support
|
|
186
197
|
|
|
187
198
|
# isolated CLI install (recommended for command-line use)
|
|
188
199
|
pipx install zipcheck
|
|
200
|
+
pipx install 'zipcheck[aes]'
|
|
189
201
|
# or
|
|
190
|
-
uv tool install zipcheck
|
|
202
|
+
uv tool install 'zipcheck[aes]'
|
|
191
203
|
|
|
192
|
-
# Arch Linux
|
|
204
|
+
# Arch Linux (python-pyzipper is an optional dependency for AES)
|
|
193
205
|
yay -S zipcheck
|
|
194
206
|
```
|
|
195
207
|
|
|
@@ -198,11 +210,11 @@ From a checkout:
|
|
|
198
210
|
```bash
|
|
199
211
|
git clone https://github.com/spaxe-dev/zipcheck
|
|
200
212
|
cd zipcheck
|
|
201
|
-
python -m pip install .
|
|
213
|
+
python -m pip install '.[aes]'
|
|
202
214
|
PYTHONPATH=src python -m zipcheck file.rar # run without installing
|
|
203
215
|
```
|
|
204
216
|
|
|
205
|
-
Container:
|
|
217
|
+
Container (the bundled image includes the AES extra):
|
|
206
218
|
|
|
207
219
|
```bash
|
|
208
220
|
docker build -t zipcheck .
|
|
@@ -240,6 +252,12 @@ zipcheck -v archive.7z
|
|
|
240
252
|
|
|
241
253
|
# production pre-flight: measure real output and fail closed
|
|
242
254
|
zipcheck --production --verify-limit 256MiB --timeout 30 --json upload.zip
|
|
255
|
+
|
|
256
|
+
# encrypted ZIP: prompt for the password (not visible in shell history)
|
|
257
|
+
zipcheck --ask-password --production secret.zip
|
|
258
|
+
|
|
259
|
+
# encrypted ZIP in a script: read the password from a file
|
|
260
|
+
zipcheck --password-file /run/secrets/zip-password --json secret.zip
|
|
243
261
|
```
|
|
244
262
|
|
|
245
263
|
Options:
|
|
@@ -257,6 +275,9 @@ Options:
|
|
|
257
275
|
| `--verify-limit SIZE` | `512MiB` | Maximum output produced by verification |
|
|
258
276
|
| `--require-verified` | off | Make unverifiable files `unknown` (implies `--verify`) |
|
|
259
277
|
| `--production` | off | Fail-closed preset: implies `--verify --require-verified` |
|
|
278
|
+
| `--password TEXT` | none | Password for encrypted ZIP members (visible in shell history/process list) |
|
|
279
|
+
| `--password-file PATH` | none | Read the ZIP password from the first line of a file |
|
|
280
|
+
| `--ask-password` | off | Prompt for the ZIP password on the terminal |
|
|
260
281
|
| `--ratio FLOAT` | `100` | Expansion ratio considered suspicious |
|
|
261
282
|
| `--dangerous-ratio FLOAT` | `1000` | Expansion ratio considered dangerous |
|
|
262
283
|
| `--min-size SIZE` | `16MiB` | Ignore ratios below this declared output |
|
|
@@ -301,7 +322,7 @@ verdict; `HIGH`/`CRITICAL` → *dangerous*, `MEDIUM` → *suspicious*.
|
|
|
301
322
|
| `ENTRY_COUNT_MISMATCH` | HIGH / MEDIUM | The central directory contains more (HIGH) or fewer (MEDIUM) entries than the EOCD counts |
|
|
302
323
|
| `NESTED_BOMB` / `NESTED_SUSPICIOUS` | HIGH / MEDIUM | A nested archive has that verdict |
|
|
303
324
|
| `INSPECTION_INCOMPLETE` | MEDIUM | Nested inspection was incomplete (caps, truncation, opaque or unknown nested members); static verdict is `unknown` |
|
|
304
|
-
| `ENCRYPTED_NESTED_ARCHIVE` | MEDIUM | Encrypted members look like archives and
|
|
325
|
+
| `ENCRYPTED_NESTED_ARCHIVE` | MEDIUM | Encrypted members look like archives and no password was supplied |
|
|
305
326
|
| `ISIZE_UNDERSTATED` | LOW | The measured gzip output exceeds the final member's `ISIZE` trailer |
|
|
306
327
|
| `METADATA_UNDERSTATED` | HIGH (ZIP) / MEDIUM (streams) | Real decompressed output exceeds the declared size |
|
|
307
328
|
| `METADATA_OVERSTATED` | MEDIUM | Declared size exceeds the real output |
|
|
@@ -347,10 +368,50 @@ When verification was requested but did not cover the whole file, an `UNVERIFIED
|
|
|
347
368
|
- `--production` → shorthand for `--verify --require-verified`.
|
|
348
369
|
|
|
349
370
|
This measures content; it is **not** a sandbox. The capped decompression still
|
|
350
|
-
runs inside your process and consumes CPU and memory. Encrypted members
|
|
351
|
-
member data, and methods without a stdlib decoder remain
|
|
352
|
-
closed. The enforceable production boundary is still
|
|
353
|
-
OS quotas — see [docs/production.md](docs/production.md).
|
|
371
|
+
runs inside your process and consumes CPU and memory. Encrypted members without
|
|
372
|
+
a password, 7z/RAR member data, and methods without a stdlib decoder remain
|
|
373
|
+
unverifiable and fail closed. The enforceable production boundary is still
|
|
374
|
+
isolated extraction with OS quotas — see [docs/production.md](docs/production.md).
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Encrypted ZIPs
|
|
379
|
+
|
|
380
|
+
ZIP encryption hides member **content**, not the central directory: names,
|
|
381
|
+
sizes, compression methods and the encryption flag stay readable. That is why a
|
|
382
|
+
password-protected bomb still shows up as a ZIP and can be measured statically;
|
|
383
|
+
what it hides is the *real* expansion and any nested archives inside each
|
|
384
|
+
encrypted member.
|
|
385
|
+
|
|
386
|
+
Pass one of the three password options to let zipcheck decrypt members while it
|
|
387
|
+
scans:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
zipcheck --ask-password bomb.zip
|
|
391
|
+
zipcheck --password-file /run/secrets/pw bomb.zip
|
|
392
|
+
zipcheck --password '42' bomb.zip # convenient, but leaks via argv/history
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- Both **WinZip AES** (AE-1/AE-2) and legacy **ZipCrypto** are supported.
|
|
396
|
+
AES needs the optional `aes` extra (`pip install 'zipcheck[aes]'`); ZipCrypto
|
|
397
|
+
is handled by the standard library.
|
|
398
|
+
- Decryption is bounded exactly like ordinary scanning: 64 KiB read chunks, the
|
|
399
|
+
per-file timeout, the nested capture limits, and `--verify-limit`. Decrypted
|
|
400
|
+
bytes are counted or held in a bounded in-memory image; nothing is written to
|
|
401
|
+
disk.
|
|
402
|
+
- Zipcheck does not brute-force or guess passwords. A wrong password, a missing
|
|
403
|
+
AES backend, or an unsupported encryption method makes the affected members
|
|
404
|
+
uninspectable, so static mode reports `unknown` and `--production` rejects the
|
|
405
|
+
file. It never falls back to `clean`.
|
|
406
|
+
- Passwords are never logged, never included in JSON/notes, and never stored in
|
|
407
|
+
`Limits` representations. Prefer `--ask-password` or `--password-file`; using
|
|
408
|
+
`--password` exposes the value to shell history and the process list.
|
|
409
|
+
- Only ZIP encryption is handled. Encrypted 7z/RAR data and encrypted RAR
|
|
410
|
+
headers remain opaque and fail closed.
|
|
411
|
+
|
|
412
|
+
With a password, the real-world AES-encrypted `42.zip` is followed through its
|
|
413
|
+
nested layers and reported `dangerous`. Without one it stays `unknown`, which is
|
|
414
|
+
the honest answer.
|
|
354
415
|
|
|
355
416
|
---
|
|
356
417
|
|
|
@@ -462,8 +523,9 @@ verification as a top-level file, recursively.
|
|
|
462
523
|
Inspection **fails closed**. If a depth, candidate, archive-count or byte
|
|
463
524
|
budget stops the scan while archive-like members remain unexamined, if a
|
|
464
525
|
candidate can only be read as a truncated prefix, if a child scan is itself
|
|
465
|
-
unknown, or if a 7z/RAR
|
|
466
|
-
decoded, static mode reports the file `unknown`
|
|
526
|
+
unknown, or if a 7z/RAR member or an encrypted member without a password looks
|
|
527
|
+
like an archive and cannot be decoded, static mode reports the file `unknown`
|
|
528
|
+
with `INSPECTION_INCOMPLETE`.
|
|
467
529
|
It is never reported `clean` just because the scanner gave up. Verified mode
|
|
468
530
|
keeps its own contract: `suspicious`, or `unknown` under `--require-verified`.
|
|
469
531
|
|
|
@@ -474,8 +536,10 @@ When a nested bomb is found, scanning stops early and the parent is marked
|
|
|
474
536
|
download.zip!lib0.zip!lib1.zip!...!payload.bin
|
|
475
537
|
```
|
|
476
538
|
|
|
477
|
-
This is enough to catch 42.zip-style chains that are not encrypted.
|
|
478
|
-
|
|
539
|
+
This is enough to catch 42.zip-style chains that are not encrypted. If the
|
|
540
|
+
outer layers are encrypted, supply a ZIP password (see
|
|
541
|
+
[Encrypted ZIPs](#encrypted-zips)) and the same recursion runs on the decrypted
|
|
542
|
+
members; without a password those members remain opaque and fail closed.
|
|
479
543
|
|
|
480
544
|
---
|
|
481
545
|
|
|
@@ -499,6 +563,9 @@ allocate an attacker-controlled amount of memory.
|
|
|
499
563
|
- **LZMA decoder caps.** The xz path sets a 128 MiB decoder memory limit, so a
|
|
500
564
|
malicious dictionary size cannot force a huge allocation.
|
|
501
565
|
- **Wall-clock limit.** 60 seconds per file by default (`--timeout`).
|
|
566
|
+
- **Passwords stay out of results.** A password is read from the terminal or a
|
|
567
|
+
file, used only for member decryption, and excluded from `Limits` repr/equality
|
|
568
|
+
and every finding, note and JSON field. It is never written anywhere.
|
|
502
569
|
- **No recursion into data formats.** 7z and RAR member data is never decoded,
|
|
503
570
|
so a solid archive cannot be used to burn CPU during a scan.
|
|
504
571
|
- **No stat-then-open race.** The path is opened and `fstat`ed on the same file
|
|
@@ -540,7 +607,7 @@ used):
|
|
|
540
607
|
| gzip-compressed sparse TAR | 142 B | 1.00 TiB (inner TAR) | dangerous |
|
|
541
608
|
| non-recursive overlap ZIP | 4.2 KiB | 8 KiB | dangerous |
|
|
542
609
|
| RAR 5, 256 MiB of zeros | 11 KiB | 256 MiB | suspicious |
|
|
543
|
-
| real `42.zip` (AES-encrypted) | 41.8 KiB |
|
|
610
|
+
| real `42.zip` (AES-encrypted, password `42`) | 41.8 KiB | 4 GiB at depth 6 | dangerous (exit 2); `unknown` without the password |
|
|
544
611
|
| 7-Zip encoded header, 50 files | 729 B | 790 B | clean (correct) |
|
|
545
612
|
| real RAR 4 archive | 336 B | 48 B | clean (correct) |
|
|
546
613
|
|
|
@@ -572,21 +639,31 @@ gzip-compressed sparse TAR, and the real 42.zip — scanned in ~0.19 seconds wit
|
|
|
572
639
|
~146 MiB peak RSS (the xz decoder's 64 MiB dictionary dominates). Reproduce
|
|
573
640
|
with `tools/generate_samples.py` and `tools/benchmark.py` inside a VM/container.
|
|
574
641
|
|
|
642
|
+
With the real `42.zip` password (`42`) supplied via `--password-file`, the same
|
|
643
|
+
file is reported `dangerous` (exit `2`) in ~6 milliseconds: the outer AES layer
|
|
644
|
+
is decrypted in memory and the nested chain is followed far enough to find a
|
|
645
|
+
layer declaring 4 GiB. `--production` also returns `dangerous`; the deeper
|
|
646
|
+
layers are not fully verifiable within the capture caps, but positive evidence
|
|
647
|
+
outranks incompleteness. Without the password the file remains `unknown`
|
|
648
|
+
(exit `3`).
|
|
649
|
+
|
|
575
650
|
---
|
|
576
651
|
|
|
577
652
|
## Limitations and honest caveats
|
|
578
653
|
|
|
579
|
-
- **Encrypted
|
|
580
|
-
|
|
581
|
-
`
|
|
582
|
-
|
|
654
|
+
- **Encrypted ZIPs need a password.** AES (WinZip) requires the `aes` extra;
|
|
655
|
+
ZipCrypto works in the base install. Without `--password`,
|
|
656
|
+
`--password-file` or `--ask-password`, encrypted members remain opaque and
|
|
657
|
+
archive-like ones make the file `unknown`. A wrong password, a missing AES
|
|
658
|
+
backend, or an unsupported method also fails closed — zipcheck does not
|
|
659
|
+
brute-force passwords.
|
|
583
660
|
- **Headers can lie.** zipcheck flags entries hidden from the EOCD count,
|
|
584
661
|
conflicting local/central headers, forged sizes that leave the real stream in
|
|
585
662
|
an unexplained gap, and measured output larger than the declared size when
|
|
586
663
|
the tail probe runs. A forgery arranged to look contiguous, or one outside
|
|
587
664
|
the probe caps, cannot be proven from metadata; `--verify` measures it.
|
|
588
|
-
Formats the verifier cannot decode (7z, RAR, encrypted members
|
|
589
|
-
and fail closed.
|
|
665
|
+
Formats the verifier cannot decode (7z, RAR, encrypted members without a
|
|
666
|
+
password) remain opaque and fail closed.
|
|
590
667
|
- **Static is not verification.** The ZIP tail probe only examines members
|
|
591
668
|
whose declared data ends before the next structure, and is capped at 256
|
|
592
669
|
members, 64 KiB compressed each and 4 MiB of total output. Its absence is
|
|
@@ -630,7 +707,7 @@ sample or a hex description — see [SECURITY.md](SECURITY.md).
|
|
|
630
707
|
```json
|
|
631
708
|
{
|
|
632
709
|
"tool": "zipcheck",
|
|
633
|
-
"version": "0.
|
|
710
|
+
"version": "0.3.0",
|
|
634
711
|
"elapsed_ms": 3.1,
|
|
635
712
|
"summary": {"clean": 0, "suspicious": 0, "dangerous": 1, "unknown": 0},
|
|
636
713
|
"results": [
|
|
@@ -690,6 +767,7 @@ Layout:
|
|
|
690
767
|
src/zipcheck/
|
|
691
768
|
engine.py # orchestration, policy, nested inspection
|
|
692
769
|
verify.py # bounded content verification (counting sink)
|
|
770
|
+
decrypt.py # bounded ZIP member decryption (AES/ZipCrypto)
|
|
693
771
|
model.py # facts/findings/results
|
|
694
772
|
limits.py # caps and thresholds
|
|
695
773
|
detect.py # magic/extension routing
|
|
@@ -43,6 +43,7 @@ $ zipcheck suspicious.zip
|
|
|
43
43
|
- [Usage](#usage)
|
|
44
44
|
- [Detection rules](#detection-rules)
|
|
45
45
|
- [Content verification](#content-verification)
|
|
46
|
+
- [Encrypted ZIPs](#encrypted-zips)
|
|
46
47
|
- [How each format is measured](#how-each-format-is-measured)
|
|
47
48
|
- [Nested archives and deep bombs](#nested-archives-and-deep-bombs)
|
|
48
49
|
- [Safety model](#safety-model)
|
|
@@ -99,10 +100,11 @@ examples.
|
|
|
99
100
|
## What it detects
|
|
100
101
|
|
|
101
102
|
- **ZIP / ZIP64** — declared expansion, per-entry impossible ratios, overlapping
|
|
102
|
-
or shared entries, encrypted archive members
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
compressed
|
|
103
|
+
or shared entries, encrypted archive members (decrypted and followed when a
|
|
104
|
+
password is supplied), member count, contradictory local/central headers,
|
|
105
|
+
entries hidden from the EOCD count, and a bounded stream-tail probe that
|
|
106
|
+
catches a compressed stream extending past a forged compressed size when it
|
|
107
|
+
leaves an unexplained gap.
|
|
106
108
|
- **TAR** — huge declared members, sparse members, truncated archives, PAX and
|
|
107
109
|
GNU long names, nested archives.
|
|
108
110
|
- **gzip** — every stream is measured with a bounded multi-member probe, so
|
|
@@ -118,11 +120,12 @@ examples.
|
|
|
118
120
|
unknown.
|
|
119
121
|
- **Nested archives** — ZIP, TAR, gzip, bzip2 and xz are followed up to a
|
|
120
122
|
configurable depth. Archive-like members inside 7z/RAR, encrypted archive-like
|
|
121
|
-
members, and chains stopped by a cap are reported `unknown`
|
|
122
|
-
silently skipped.
|
|
123
|
+
members without a password, and chains stopped by a cap are reported `unknown`
|
|
124
|
+
instead of being silently skipped.
|
|
123
125
|
- **Content verification** — optional bounded decompression to a counting sink
|
|
124
126
|
catches headers that understate the real size (ZIP stored/deflate/bzip2/LZMA,
|
|
125
|
-
gzip, bzip2, xz/LZMA; TAR plan validation)
|
|
127
|
+
gzip, bzip2, xz/LZMA; TAR plan validation), including encrypted ZIP members
|
|
128
|
+
when a password is provided.
|
|
126
129
|
|
|
127
130
|
Every result is one of: `clean`, `suspicious`, `dangerous`, `unknown`.
|
|
128
131
|
**`unknown` is not a safe verdict** — it means the scanner could not prove
|
|
@@ -138,17 +141,23 @@ anything either way.
|
|
|
138
141
|
|
|
139
142
|
## Install
|
|
140
143
|
|
|
141
|
-
Requires Python 3.10+.
|
|
144
|
+
Requires Python 3.10+. There are no required runtime dependencies.
|
|
145
|
+
|
|
146
|
+
WinZip AES-encrypted ZIPs need the optional `aes` extra, which installs
|
|
147
|
+
[`pyzipper`](https://pypi.org/project/pyzipper/). Legacy ZipCrypto archives work
|
|
148
|
+
without it.
|
|
142
149
|
|
|
143
150
|
```bash
|
|
144
151
|
pip install zipcheck
|
|
152
|
+
pip install 'zipcheck[aes]' # + AES-encrypted ZIP support
|
|
145
153
|
|
|
146
154
|
# isolated CLI install (recommended for command-line use)
|
|
147
155
|
pipx install zipcheck
|
|
156
|
+
pipx install 'zipcheck[aes]'
|
|
148
157
|
# or
|
|
149
|
-
uv tool install zipcheck
|
|
158
|
+
uv tool install 'zipcheck[aes]'
|
|
150
159
|
|
|
151
|
-
# Arch Linux
|
|
160
|
+
# Arch Linux (python-pyzipper is an optional dependency for AES)
|
|
152
161
|
yay -S zipcheck
|
|
153
162
|
```
|
|
154
163
|
|
|
@@ -157,11 +166,11 @@ From a checkout:
|
|
|
157
166
|
```bash
|
|
158
167
|
git clone https://github.com/spaxe-dev/zipcheck
|
|
159
168
|
cd zipcheck
|
|
160
|
-
python -m pip install .
|
|
169
|
+
python -m pip install '.[aes]'
|
|
161
170
|
PYTHONPATH=src python -m zipcheck file.rar # run without installing
|
|
162
171
|
```
|
|
163
172
|
|
|
164
|
-
Container:
|
|
173
|
+
Container (the bundled image includes the AES extra):
|
|
165
174
|
|
|
166
175
|
```bash
|
|
167
176
|
docker build -t zipcheck .
|
|
@@ -199,6 +208,12 @@ zipcheck -v archive.7z
|
|
|
199
208
|
|
|
200
209
|
# production pre-flight: measure real output and fail closed
|
|
201
210
|
zipcheck --production --verify-limit 256MiB --timeout 30 --json upload.zip
|
|
211
|
+
|
|
212
|
+
# encrypted ZIP: prompt for the password (not visible in shell history)
|
|
213
|
+
zipcheck --ask-password --production secret.zip
|
|
214
|
+
|
|
215
|
+
# encrypted ZIP in a script: read the password from a file
|
|
216
|
+
zipcheck --password-file /run/secrets/zip-password --json secret.zip
|
|
202
217
|
```
|
|
203
218
|
|
|
204
219
|
Options:
|
|
@@ -216,6 +231,9 @@ Options:
|
|
|
216
231
|
| `--verify-limit SIZE` | `512MiB` | Maximum output produced by verification |
|
|
217
232
|
| `--require-verified` | off | Make unverifiable files `unknown` (implies `--verify`) |
|
|
218
233
|
| `--production` | off | Fail-closed preset: implies `--verify --require-verified` |
|
|
234
|
+
| `--password TEXT` | none | Password for encrypted ZIP members (visible in shell history/process list) |
|
|
235
|
+
| `--password-file PATH` | none | Read the ZIP password from the first line of a file |
|
|
236
|
+
| `--ask-password` | off | Prompt for the ZIP password on the terminal |
|
|
219
237
|
| `--ratio FLOAT` | `100` | Expansion ratio considered suspicious |
|
|
220
238
|
| `--dangerous-ratio FLOAT` | `1000` | Expansion ratio considered dangerous |
|
|
221
239
|
| `--min-size SIZE` | `16MiB` | Ignore ratios below this declared output |
|
|
@@ -260,7 +278,7 @@ verdict; `HIGH`/`CRITICAL` → *dangerous*, `MEDIUM` → *suspicious*.
|
|
|
260
278
|
| `ENTRY_COUNT_MISMATCH` | HIGH / MEDIUM | The central directory contains more (HIGH) or fewer (MEDIUM) entries than the EOCD counts |
|
|
261
279
|
| `NESTED_BOMB` / `NESTED_SUSPICIOUS` | HIGH / MEDIUM | A nested archive has that verdict |
|
|
262
280
|
| `INSPECTION_INCOMPLETE` | MEDIUM | Nested inspection was incomplete (caps, truncation, opaque or unknown nested members); static verdict is `unknown` |
|
|
263
|
-
| `ENCRYPTED_NESTED_ARCHIVE` | MEDIUM | Encrypted members look like archives and
|
|
281
|
+
| `ENCRYPTED_NESTED_ARCHIVE` | MEDIUM | Encrypted members look like archives and no password was supplied |
|
|
264
282
|
| `ISIZE_UNDERSTATED` | LOW | The measured gzip output exceeds the final member's `ISIZE` trailer |
|
|
265
283
|
| `METADATA_UNDERSTATED` | HIGH (ZIP) / MEDIUM (streams) | Real decompressed output exceeds the declared size |
|
|
266
284
|
| `METADATA_OVERSTATED` | MEDIUM | Declared size exceeds the real output |
|
|
@@ -306,10 +324,50 @@ When verification was requested but did not cover the whole file, an `UNVERIFIED
|
|
|
306
324
|
- `--production` → shorthand for `--verify --require-verified`.
|
|
307
325
|
|
|
308
326
|
This measures content; it is **not** a sandbox. The capped decompression still
|
|
309
|
-
runs inside your process and consumes CPU and memory. Encrypted members
|
|
310
|
-
member data, and methods without a stdlib decoder remain
|
|
311
|
-
closed. The enforceable production boundary is still
|
|
312
|
-
OS quotas — see [docs/production.md](docs/production.md).
|
|
327
|
+
runs inside your process and consumes CPU and memory. Encrypted members without
|
|
328
|
+
a password, 7z/RAR member data, and methods without a stdlib decoder remain
|
|
329
|
+
unverifiable and fail closed. The enforceable production boundary is still
|
|
330
|
+
isolated extraction with OS quotas — see [docs/production.md](docs/production.md).
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Encrypted ZIPs
|
|
335
|
+
|
|
336
|
+
ZIP encryption hides member **content**, not the central directory: names,
|
|
337
|
+
sizes, compression methods and the encryption flag stay readable. That is why a
|
|
338
|
+
password-protected bomb still shows up as a ZIP and can be measured statically;
|
|
339
|
+
what it hides is the *real* expansion and any nested archives inside each
|
|
340
|
+
encrypted member.
|
|
341
|
+
|
|
342
|
+
Pass one of the three password options to let zipcheck decrypt members while it
|
|
343
|
+
scans:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
zipcheck --ask-password bomb.zip
|
|
347
|
+
zipcheck --password-file /run/secrets/pw bomb.zip
|
|
348
|
+
zipcheck --password '42' bomb.zip # convenient, but leaks via argv/history
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- Both **WinZip AES** (AE-1/AE-2) and legacy **ZipCrypto** are supported.
|
|
352
|
+
AES needs the optional `aes` extra (`pip install 'zipcheck[aes]'`); ZipCrypto
|
|
353
|
+
is handled by the standard library.
|
|
354
|
+
- Decryption is bounded exactly like ordinary scanning: 64 KiB read chunks, the
|
|
355
|
+
per-file timeout, the nested capture limits, and `--verify-limit`. Decrypted
|
|
356
|
+
bytes are counted or held in a bounded in-memory image; nothing is written to
|
|
357
|
+
disk.
|
|
358
|
+
- Zipcheck does not brute-force or guess passwords. A wrong password, a missing
|
|
359
|
+
AES backend, or an unsupported encryption method makes the affected members
|
|
360
|
+
uninspectable, so static mode reports `unknown` and `--production` rejects the
|
|
361
|
+
file. It never falls back to `clean`.
|
|
362
|
+
- Passwords are never logged, never included in JSON/notes, and never stored in
|
|
363
|
+
`Limits` representations. Prefer `--ask-password` or `--password-file`; using
|
|
364
|
+
`--password` exposes the value to shell history and the process list.
|
|
365
|
+
- Only ZIP encryption is handled. Encrypted 7z/RAR data and encrypted RAR
|
|
366
|
+
headers remain opaque and fail closed.
|
|
367
|
+
|
|
368
|
+
With a password, the real-world AES-encrypted `42.zip` is followed through its
|
|
369
|
+
nested layers and reported `dangerous`. Without one it stays `unknown`, which is
|
|
370
|
+
the honest answer.
|
|
313
371
|
|
|
314
372
|
---
|
|
315
373
|
|
|
@@ -421,8 +479,9 @@ verification as a top-level file, recursively.
|
|
|
421
479
|
Inspection **fails closed**. If a depth, candidate, archive-count or byte
|
|
422
480
|
budget stops the scan while archive-like members remain unexamined, if a
|
|
423
481
|
candidate can only be read as a truncated prefix, if a child scan is itself
|
|
424
|
-
unknown, or if a 7z/RAR
|
|
425
|
-
decoded, static mode reports the file `unknown`
|
|
482
|
+
unknown, or if a 7z/RAR member or an encrypted member without a password looks
|
|
483
|
+
like an archive and cannot be decoded, static mode reports the file `unknown`
|
|
484
|
+
with `INSPECTION_INCOMPLETE`.
|
|
426
485
|
It is never reported `clean` just because the scanner gave up. Verified mode
|
|
427
486
|
keeps its own contract: `suspicious`, or `unknown` under `--require-verified`.
|
|
428
487
|
|
|
@@ -433,8 +492,10 @@ When a nested bomb is found, scanning stops early and the parent is marked
|
|
|
433
492
|
download.zip!lib0.zip!lib1.zip!...!payload.bin
|
|
434
493
|
```
|
|
435
494
|
|
|
436
|
-
This is enough to catch 42.zip-style chains that are not encrypted.
|
|
437
|
-
|
|
495
|
+
This is enough to catch 42.zip-style chains that are not encrypted. If the
|
|
496
|
+
outer layers are encrypted, supply a ZIP password (see
|
|
497
|
+
[Encrypted ZIPs](#encrypted-zips)) and the same recursion runs on the decrypted
|
|
498
|
+
members; without a password those members remain opaque and fail closed.
|
|
438
499
|
|
|
439
500
|
---
|
|
440
501
|
|
|
@@ -458,6 +519,9 @@ allocate an attacker-controlled amount of memory.
|
|
|
458
519
|
- **LZMA decoder caps.** The xz path sets a 128 MiB decoder memory limit, so a
|
|
459
520
|
malicious dictionary size cannot force a huge allocation.
|
|
460
521
|
- **Wall-clock limit.** 60 seconds per file by default (`--timeout`).
|
|
522
|
+
- **Passwords stay out of results.** A password is read from the terminal or a
|
|
523
|
+
file, used only for member decryption, and excluded from `Limits` repr/equality
|
|
524
|
+
and every finding, note and JSON field. It is never written anywhere.
|
|
461
525
|
- **No recursion into data formats.** 7z and RAR member data is never decoded,
|
|
462
526
|
so a solid archive cannot be used to burn CPU during a scan.
|
|
463
527
|
- **No stat-then-open race.** The path is opened and `fstat`ed on the same file
|
|
@@ -499,7 +563,7 @@ used):
|
|
|
499
563
|
| gzip-compressed sparse TAR | 142 B | 1.00 TiB (inner TAR) | dangerous |
|
|
500
564
|
| non-recursive overlap ZIP | 4.2 KiB | 8 KiB | dangerous |
|
|
501
565
|
| RAR 5, 256 MiB of zeros | 11 KiB | 256 MiB | suspicious |
|
|
502
|
-
| real `42.zip` (AES-encrypted) | 41.8 KiB |
|
|
566
|
+
| real `42.zip` (AES-encrypted, password `42`) | 41.8 KiB | 4 GiB at depth 6 | dangerous (exit 2); `unknown` without the password |
|
|
503
567
|
| 7-Zip encoded header, 50 files | 729 B | 790 B | clean (correct) |
|
|
504
568
|
| real RAR 4 archive | 336 B | 48 B | clean (correct) |
|
|
505
569
|
|
|
@@ -531,21 +595,31 @@ gzip-compressed sparse TAR, and the real 42.zip — scanned in ~0.19 seconds wit
|
|
|
531
595
|
~146 MiB peak RSS (the xz decoder's 64 MiB dictionary dominates). Reproduce
|
|
532
596
|
with `tools/generate_samples.py` and `tools/benchmark.py` inside a VM/container.
|
|
533
597
|
|
|
598
|
+
With the real `42.zip` password (`42`) supplied via `--password-file`, the same
|
|
599
|
+
file is reported `dangerous` (exit `2`) in ~6 milliseconds: the outer AES layer
|
|
600
|
+
is decrypted in memory and the nested chain is followed far enough to find a
|
|
601
|
+
layer declaring 4 GiB. `--production` also returns `dangerous`; the deeper
|
|
602
|
+
layers are not fully verifiable within the capture caps, but positive evidence
|
|
603
|
+
outranks incompleteness. Without the password the file remains `unknown`
|
|
604
|
+
(exit `3`).
|
|
605
|
+
|
|
534
606
|
---
|
|
535
607
|
|
|
536
608
|
## Limitations and honest caveats
|
|
537
609
|
|
|
538
|
-
- **Encrypted
|
|
539
|
-
|
|
540
|
-
`
|
|
541
|
-
|
|
610
|
+
- **Encrypted ZIPs need a password.** AES (WinZip) requires the `aes` extra;
|
|
611
|
+
ZipCrypto works in the base install. Without `--password`,
|
|
612
|
+
`--password-file` or `--ask-password`, encrypted members remain opaque and
|
|
613
|
+
archive-like ones make the file `unknown`. A wrong password, a missing AES
|
|
614
|
+
backend, or an unsupported method also fails closed — zipcheck does not
|
|
615
|
+
brute-force passwords.
|
|
542
616
|
- **Headers can lie.** zipcheck flags entries hidden from the EOCD count,
|
|
543
617
|
conflicting local/central headers, forged sizes that leave the real stream in
|
|
544
618
|
an unexplained gap, and measured output larger than the declared size when
|
|
545
619
|
the tail probe runs. A forgery arranged to look contiguous, or one outside
|
|
546
620
|
the probe caps, cannot be proven from metadata; `--verify` measures it.
|
|
547
|
-
Formats the verifier cannot decode (7z, RAR, encrypted members
|
|
548
|
-
and fail closed.
|
|
621
|
+
Formats the verifier cannot decode (7z, RAR, encrypted members without a
|
|
622
|
+
password) remain opaque and fail closed.
|
|
549
623
|
- **Static is not verification.** The ZIP tail probe only examines members
|
|
550
624
|
whose declared data ends before the next structure, and is capped at 256
|
|
551
625
|
members, 64 KiB compressed each and 4 MiB of total output. Its absence is
|
|
@@ -589,7 +663,7 @@ sample or a hex description — see [SECURITY.md](SECURITY.md).
|
|
|
589
663
|
```json
|
|
590
664
|
{
|
|
591
665
|
"tool": "zipcheck",
|
|
592
|
-
"version": "0.
|
|
666
|
+
"version": "0.3.0",
|
|
593
667
|
"elapsed_ms": 3.1,
|
|
594
668
|
"summary": {"clean": 0, "suspicious": 0, "dangerous": 1, "unknown": 0},
|
|
595
669
|
"results": [
|
|
@@ -649,6 +723,7 @@ Layout:
|
|
|
649
723
|
src/zipcheck/
|
|
650
724
|
engine.py # orchestration, policy, nested inspection
|
|
651
725
|
verify.py # bounded content verification (counting sink)
|
|
726
|
+
decrypt.py # bounded ZIP member decryption (AES/ZipCrypto)
|
|
652
727
|
model.py # facts/findings/results
|
|
653
728
|
limits.py # caps and thresholds
|
|
654
729
|
detect.py # magic/extension routing
|
|
@@ -8,11 +8,14 @@ or a guarantee that an archive is safe.
|
|
|
8
8
|
|
|
9
9
|
The scanner itself is designed to be safe against hostile inputs:
|
|
10
10
|
|
|
11
|
-
- it never extracts archive members to disk
|
|
11
|
+
- it never extracts archive members to disk; decrypted ZIP data is counted or
|
|
12
|
+
held in a bounded in-memory image only;
|
|
12
13
|
- it never invokes `unzip`, `7z`, `tar`, `rar`, or any external program;
|
|
13
|
-
- it caps metadata reads, entry counts, nested depth,
|
|
14
|
+
- it caps metadata reads, entry counts, nested depth, decrypted bytes and
|
|
14
15
|
wall-clock time;
|
|
15
|
-
- it checks 7z/xz decoder dictionary sizes against a memory cap
|
|
16
|
+
- it checks 7z/xz decoder dictionary sizes against a memory cap;
|
|
17
|
+
- it keeps a supplied ZIP password out of findings, notes, JSON and `Limits`
|
|
18
|
+
representations, and never writes it anywhere.
|
|
16
19
|
|
|
17
20
|
Those controls protect against resource exhaustion during the scan. They say
|
|
18
21
|
nothing about what happens if *you* later extract the archive with another tool.
|
|
@@ -21,7 +24,7 @@ memory and time quotas.
|
|
|
21
24
|
|
|
22
25
|
## Supported versions
|
|
23
26
|
|
|
24
|
-
Security fixes are applied to the latest `0.
|
|
27
|
+
Security fixes are applied to the latest `0.3.x` release.
|
|
25
28
|
|
|
26
29
|
## Reporting a vulnerability
|
|
27
30
|
|
|
@@ -39,11 +42,16 @@ Useful reports include:
|
|
|
39
42
|
- a bounded-decompression bypass that exceeds the documented caps;
|
|
40
43
|
- a parser confusion that makes a clearly dangerous archive report `clean`;
|
|
41
44
|
- path traversal or command execution (there should be none);
|
|
42
|
-
- a way to make the scanner write to disk or spawn a process
|
|
45
|
+
- a way to make the scanner write to disk or spawn a process;
|
|
46
|
+
- a way to make a supplied password appear in output, logs or process metadata
|
|
47
|
+
beyond the documented `--password` argv caveat.
|
|
43
48
|
|
|
44
49
|
## Known non-goals
|
|
45
50
|
|
|
46
|
-
-
|
|
51
|
+
- Brute-forcing, guessing or recovering passwords.
|
|
52
|
+
- Decrypting password-protected 7z/RAR data or encrypted RAR headers. ZIP
|
|
53
|
+
encryption is supported when a password is supplied and, for AES, the
|
|
54
|
+
optional `pyzipper` extra is installed.
|
|
47
55
|
- Proving that an archive is benign. `clean` means "no rule fired", not "safe".
|
|
48
56
|
- Detecting bombs in formats that are not parsed.
|
|
49
57
|
- Protecting a machine from extraction performed by a different program.
|