From 4f66ba1c20de0d01ccc6bf4dc11470f26ca6a01c Mon Sep 17 00:00:00 2001 From: Armijn Hemel Date: Thu, 15 Jan 2026 21:23:21 +0100 Subject: [PATCH 1/2] add testdata for files with regular ZIP passwords. Different unpackers unpack these files differently, sometimes leaving empty files and directories. --- testdata/readme.password | 86 ++++++++++++++++++++++++++++++++++++++ testdata/zip_password.zip | Bin 0 -> 482 bytes 2 files changed, 86 insertions(+) create mode 100644 testdata/readme.password create mode 100644 testdata/zip_password.zip diff --git a/testdata/readme.password b/testdata/readme.password new file mode 100644 index 0000000..e33f0d6 --- /dev/null +++ b/testdata/readme.password @@ -0,0 +1,86 @@ +# Encryption + +Entries in ZIP files can be encrypted with a variety of methods. The standard +password encryption in ZIP is weak and prone to a known plaintext attack. If an +entry is encryted with this encryption method then the "encryption" bit in the +general purpose bit flag should have been set. + +In case an encrypted entry is found and there is no password available then it +still possible to do structural checks (extract file name, CRC32, and so on) +and verify if the data is sound and skip the encrypted data, while unpacking +data that has not been encrypted (such as directories, which are only stored). + +This can be easily demonstrated by building an encrypted ZIP file with a file +inside a directory: + +``` +$ zip -r zip_password.zip dir -e -Ptest + adding: dir/ (stored 0%) + adding: dir/bar (stored 0%) + adding: dir/empty/ (stored 0%) +``` + +and then extracting it with `unzip`. If the correct password is not given the +directory (which has not been encrypted, but merely stored) will still be +unpacked/created: + +``` +$ unzip zip_password.zip +Archive: zip_password.zip + creating: dir/ +[zip_password.zip] dir/bar password: + skipping: dir/bar incorrect password + creating: dir/empty/ +``` + +and no files will have been unpacked: + +``` +$ find dir/ -type f | wc -l +0 +``` + +Interestingly, and unlike `unzip`, when running `p7zip` an empty placeholder +file will be created: + +``` +$ 7z x zip_password.zip + +7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21 +p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,8 CPUs Intel(R) Core(TM) i7-6770HQ CPU @ 2.60GHz (506E3),ASM,AES-NI) + +Scanning the drive for archives: +1 file, 482 bytes (1 KiB) + +Extracting archive: zip_password.zip +-- +Path = zip_password.zip +Type = zip +Physical Size = 482 + + +Enter password (will not be echoed): +ERROR: Wrong password : dir/bar + +Sub items Errors: 1 + +Archives with Errors: 1 + +Sub items Errors: 1 +``` +The directory `test` will now contain an empty file: + +``` +$ find dir/ -type f | wc -l +1 +$ du -h dir/ +0 dir/empty +0 dir/ +$ du -h dir/bar +0 dir/bar +``` + +Other encryption methods are stronger. Depending on the encryption method the +encryption bit flag might or might not be set. For example: for AE-x it will +be set (APPENDIX E), while for other encryption methods it might not. The flag +should not be used as the sole indicator of encryption. diff --git a/testdata/zip_password.zip b/testdata/zip_password.zip new file mode 100644 index 0000000000000000000000000000000000000000..304a0d04d636848cf665dcd4a73bde62c655adc6 GIT binary patch literal 482 zcmWIWW@h1H0D+e)^<%&cD8a%Y!;q3$q#qi>$-sQgD>HMNcV=d31vdjD%L`@(1~3r- z*3AjjF*BgUe%|c5Dj^`74T#yHx|0%%zy@rpec%VeXa-Dj?O6Jj7L-&%oLN@)z;8A-XEHL$G2`;41k^wQhPRF& zCdAXM5Kp7IH^3WW5_T^`OkxC@v82%sY7#gAfQDi5Jj^hVrI?;WcGg0uVKA=(%>#KA U!*Q&jpl4tP!skHxDTu=W0Jf4|iU0rr literal 0 HcmV?d00001 From 3c1d3554e7b2a1813d5fd09e0e1538d5536d9fcd Mon Sep 17 00:00:00 2001 From: Armijn Hemel Date: Thu, 15 Jan 2026 21:30:35 +0100 Subject: [PATCH 2/2] add note about directories never being encrypted --- testdata/readme.password | 60 +++ .../ziplinter__test__zip_password.zip.snap | 349 ++++++++++++++++++ 2 files changed, 409 insertions(+) create mode 100644 ziplinter/src/snapshots/ziplinter__test__zip_password.zip.snap diff --git a/testdata/readme.password b/testdata/readme.password index e33f0d6..c785bf1 100644 --- a/testdata/readme.password +++ b/testdata/readme.password @@ -80,6 +80,66 @@ $ du -h dir/bar 0 dir/bar ``` +Directories seem to be not encrypted, so they can always be created, as can +be seen in the field `file security status` when using `zipinfo` on a file and +searching for a directory entry: + +``` +Archive: zip_password.zip +There is no zipfile comment. + +End-of-central-directory record: +------------------------------- + + Zip archive file size: 482 (00000000000001E2h) + Actual end-cent-dir record offset: 460 (00000000000001CCh) + Expected end-cent-dir record offset: 460 (00000000000001CCh) + (based on the length of the central directory and its expected offset) + + This zipfile constitutes the sole disk of a single-part archive; its + central directory contains 3 entries. + The central directory is 231 (00000000000000E7h) bytes long, + and its (expected) offset in bytes from the beginning of the zipfile + is 229 (00000000000000E5h). + + +Central directory entry #1: +--------------------------- + + dir/ + + offset of local header from start of archive: 0 + (0000000000000000h) bytes + file system or operating system of origin: Unix + version of encoding software: 3.0 + minimum file system compatibility required: MS-DOS, OS/2 or NT FAT + minimum software version required to extract: 1.0 + compression method: none (stored) + file security status: not encrypted + extended local header: no + file last modified on (DOS date/time): 2026 Jan 15 21:15:18 + file last modified on (UT extra field modtime): 2026 Jan 15 21:15:18 local + file last modified on (UT extra field modtime): 2026 Jan 15 20:15:18 UTC + 32-bit CRC value (hex): 00000000 + compressed size: 0 bytes + uncompressed size: 0 bytes + length of filename: 4 characters + length of extra field: 24 bytes + length of file comment: 0 characters + disk number on which file begins: disk 1 + apparent file type: binary + Unix file attributes (040755 octal): drwxr-xr-x + MS-DOS file attributes (10 hex): dir + + The central-directory extra field contains: + - A subfield with ID 0x5455 (universal time) and 5 data bytes. + The local extra field has UTC/GMT modification/access times. + - A subfield with ID 0x7875 (Unix UID/GID (any size)) and 11 data bytes: + 01 04 e8 03 00 00 04 e8 03 00 00. + + There is no file comment. +``` + Other encryption methods are stronger. Depending on the encryption method the encryption bit flag might or might not be set. For example: for AE-x it will be set (APPENDIX E), while for other encryption methods it might not. The flag diff --git a/ziplinter/src/snapshots/ziplinter__test__zip_password.zip.snap b/ziplinter/src/snapshots/ziplinter__test__zip_password.zip.snap new file mode 100644 index 0000000..41b537c --- /dev/null +++ b/ziplinter/src/snapshots/ziplinter__test__zip_password.zip.snap @@ -0,0 +1,349 @@ +--- +source: ziplinter/src/lib.rs +expression: result +--- +{ + "comment": "", + "contents": [ + { + "central": { + "comment": "", + "compressed_size": 0, + "crc32": 0, + "creator_version": { + "host_system": "Unix", + "version": 30 + }, + "disk_nbr_start": 0, + "external_attrs": 1106051088, + "extra": [ + 85, + 84, + 5, + 0, + 3, + 214, + 74, + 105, + 105, + 117, + 120, + 11, + 0, + 1, + 4, + 232, + 3, + 0, + 0, + 4, + 232, + 3, + 0, + 0 + ], + "flags": 0, + "header_offset": 0, + "internal_attrs": 0, + "method": "Store", + "mode": 2147484141, + "modified": "2026-01-15T20:15:18Z", + "name": "dir/", + "reader_version": { + "host_system": "MsDos", + "version": 10 + }, + "uncompressed_size": 0 + }, + "local": { + "accessed": null, + "compressed_size": 0, + "crc32": 0, + "created": null, + "extra": [ + 85, + 84, + 9, + 0, + 3, + 214, + 74, + 105, + 105, + 150, + 75, + 105, + 105, + 117, + 120, + 11, + 0, + 1, + 4, + 232, + 3, + 0, + 0, + 4, + 232, + 3, + 0, + 0 + ], + "flags": 0, + "gid": 1000, + "header_offset": 0, + "method": "Store", + "method_specific": "None", + "mode": 2147483648, + "modified": "2026-01-15T20:15:18Z", + "name": "dir/", + "reader_version": { + "host_system": "MsDos", + "version": 10 + }, + "uid": 1000, + "uncompressed_size": 0 + } + }, + { + "central": { + "comment": "", + "compressed_size": 18, + "crc32": 2055117726, + "creator_version": { + "host_system": "Unix", + "version": 30 + }, + "disk_nbr_start": 0, + "external_attrs": 2175008768, + "extra": [ + 85, + 84, + 5, + 0, + 3, + 178, + 125, + 224, + 78, + 117, + 120, + 11, + 0, + 1, + 4, + 232, + 3, + 0, + 0, + 4, + 232, + 3, + 0, + 0 + ], + "flags": 9, + "header_offset": 62, + "internal_attrs": 1, + "method": "Store", + "mode": 420, + "modified": "2011-12-08T09:04:50Z", + "name": "dir/bar", + "reader_version": { + "host_system": "MsDos", + "version": 10 + }, + "uncompressed_size": 6 + }, + "local": { + "error": "Custom { kind: Other, error: Format(WrongSize { expected: 6, actual: 18 }) }" + } + }, + { + "central": { + "comment": "", + "compressed_size": 0, + "crc32": 0, + "creator_version": { + "host_system": "Unix", + "version": 30 + }, + "disk_nbr_start": 0, + "external_attrs": 1106051088, + "extra": [ + 85, + 84, + 5, + 0, + 3, + 118, + 126, + 224, + 78, + 117, + 120, + 11, + 0, + 1, + 4, + 232, + 3, + 0, + 0, + 4, + 232, + 3, + 0, + 0 + ], + "flags": 0, + "header_offset": 161, + "internal_attrs": 0, + "method": "Store", + "mode": 2147484141, + "modified": "2011-12-08T09:08:06Z", + "name": "dir/empty/", + "reader_version": { + "host_system": "MsDos", + "version": 10 + }, + "uncompressed_size": 0 + }, + "local": { + "accessed": null, + "compressed_size": 0, + "crc32": 0, + "created": null, + "extra": [ + 85, + 84, + 9, + 0, + 3, + 118, + 126, + 224, + 78, + 155, + 75, + 105, + 105, + 117, + 120, + 11, + 0, + 1, + 4, + 232, + 3, + 0, + 0, + 4, + 232, + 3, + 0, + 0 + ], + "flags": 0, + "gid": 1000, + "header_offset": 0, + "method": "Store", + "method_specific": "None", + "mode": 2147483648, + "modified": "2011-12-08T09:08:06Z", + "name": "dir/empty/", + "reader_version": { + "host_system": "MsDos", + "version": 10 + }, + "uid": 1000, + "uncompressed_size": 0 + } + } + ], + "encoding": "Utf8", + "eocd": { + "dir": { + "inner": { + "dir_disk_nbr": 0, + "dir_records_this_disk": 3, + "directory_offset": 229, + "directory_records": 3, + "directory_size": 231, + "disk_nbr": 0 + }, + "offset": 460 + }, + "dir64": null, + "global_offset": 0 + }, + "parsed_ranges": [ + { + "contains": "end of central directory record", + "end": 482, + "start": 460 + }, + { + "contains": "central directory header", + "end": 303, + "filename": "dir/", + "start": 229 + }, + { + "contains": "central directory header", + "end": 380, + "filename": "dir/bar", + "start": 303 + }, + { + "contains": "central directory header", + "end": 460, + "filename": "dir/empty/", + "start": 454 + }, + { + "contains": "local file header", + "end": 62, + "filename": "dir/", + "start": 0 + }, + { + "contains": "file data", + "end": 62, + "filename": "dir/", + "start": 62 + }, + { + "contains": "local file header", + "end": 127, + "filename": "dir/bar", + "start": 62 + }, + { + "contains": "file data", + "end": 145, + "filename": "dir/bar", + "start": 127 + }, + { + "contains": "data descriptor", + "end": 161, + "filename": "dir/bar", + "start": 145 + }, + { + "contains": "local file header", + "end": 229, + "filename": "dir/empty/", + "start": 161 + }, + { + "contains": "file data", + "end": 229, + "filename": "dir/empty/", + "start": 229 + } + ], + "size": 482 +}