33require ( '../common' ) ;
44
55const assert = require ( 'node:assert' ) ;
6+ const fs = require ( 'node:fs' ) ;
67const zlib = require ( 'node:zlib' ) ;
78const { test } = require ( 'node:test' ) ;
9+ const tmpdir = require ( '../common/tmpdir' ) ;
810
911async function buildArchive ( entries , comment ) {
1012 const chunks = [ ] ;
@@ -47,13 +49,71 @@ test('an EOCD-looking signature inside a trailing comment is not mistaken for th
4749 // before it reaches the genuine EOCD signature; embedding 4 bytes that
4850 // look like one partway through must not be mistaken for the real record.
4951 const fakeSignature = String . fromCharCode ( 0x50 , 0x4b , 0x05 , 0x06 ) ;
50- const archive = await buildArchive ( [ entry ] , `before ${ fakeSignature } after` ) ;
52+ const archive = await buildArchive (
53+ [ entry ] , `before ${ fakeSignature } this is not a valid EOCD record after` ) ;
5154
5255 const read = [ ...zlib . ZipEntry . read ( archive ) ] ;
5356 assert . strictEqual ( read . length , 1 ) ;
5457 assert . strictEqual ( read [ 0 ] . name , 'f.txt' ) ;
5558} ) ;
5659
60+ test ( 'multiple plausible EOCD records describing different archives are rejected' , async ( ) => {
61+ const first = await buildArchive ( [
62+ await zlib . ZipEntry . create ( 'install.sh' , Buffer . from ( 'malicious' ) , { method : 'store' } ) ,
63+ ] ) ;
64+ const second = await buildArchive ( [
65+ await zlib . ZipEntry . create ( 'install.sh' , Buffer . from ( 'benign' ) , { method : 'store' } ) ,
66+ ] ) ;
67+ const archive = Buffer . concat ( [ first , second , Buffer . from ( [ 0 ] ) ] ) ;
68+ const firstEocd = first . length - 22 ;
69+
70+ // Make the first EOCD exact-to-EOF by treating the second archive and its
71+ // padding as a comment. The second EOCD remains a plausible archive end for
72+ // readers which tolerate trailing padding and select the rightmost record.
73+ archive . writeUInt16LE ( archive . length - firstEocd - 22 , firstEocd + 20 ) ;
74+
75+ const expected = {
76+ code : 'ERR_ZIP_INVALID_ARCHIVE' ,
77+ message : / a m b i g u o u s e n d o f c e n t r a l d i r e c t o r y / ,
78+ } ;
79+ assert . throws ( ( ) => [ ...zlib . ZipEntry . read ( archive ) ] , expected ) ;
80+ assert . throws ( ( ) => new zlib . ZipBuffer ( archive ) , expected ) ;
81+
82+ tmpdir . refresh ( ) ;
83+ const file = tmpdir . resolve ( 'ambiguous.zip' ) ;
84+ fs . writeFileSync ( file , archive ) ;
85+ await assert . rejects ( zlib . ZipFile . open ( file ) , expected ) ;
86+ assert . throws ( ( ) => zlib . ZipFile . openSync ( file ) , expected ) ;
87+ } ) ;
88+
89+ test ( 'an exact EOCD embedded in a genuine comment is rejected as ambiguous' , async ( ) => {
90+ const archive = await buildArchive ( [
91+ await zlib . ZipEntry . create ( 'f.txt' , Buffer . from ( 'content' ) , { method : 'store' } ) ,
92+ ] ) ;
93+ const nested = Buffer . concat ( [ archive , buildEocd ( ) ] ) ;
94+ nested . writeUInt16LE ( 22 , archive . length - 2 ) ;
95+
96+ assert . throws ( ( ) => [ ...zlib . ZipEntry . read ( nested ) ] , {
97+ code : 'ERR_ZIP_INVALID_ARCHIVE' ,
98+ message : / a m b i g u o u s e n d o f c e n t r a l d i r e c t o r y / ,
99+ } ) ;
100+ } ) ;
101+
102+ test ( 'multiple padded EOCD records are rejected as ambiguous' , async ( ) => {
103+ const first = await buildArchive ( [
104+ await zlib . ZipEntry . create ( 'a.txt' , Buffer . from ( 'first' ) , { method : 'store' } ) ,
105+ ] ) ;
106+ const second = await buildArchive ( [
107+ await zlib . ZipEntry . create ( 'b.txt' , Buffer . from ( 'second' ) , { method : 'store' } ) ,
108+ ] ) ;
109+ const archive = Buffer . concat ( [ first , second , Buffer . from ( '\0\0' ) ] ) ;
110+
111+ assert . throws ( ( ) => new zlib . ZipBuffer ( archive ) , {
112+ code : 'ERR_ZIP_INVALID_ARCHIVE' ,
113+ message : / a m b i g u o u s e n d o f c e n t r a l d i r e c t o r y / ,
114+ } ) ;
115+ } ) ;
116+
57117test ( 'a declared-size mismatch is rejected as corrupt' , async ( ) => {
58118 const entry = await zlib . ZipEntry . create ( 'f.txt' , Buffer . from ( 'hello world' ) , { method : 'store' } ) ;
59119 const archive = await buildArchive ( [ entry ] ) ;
@@ -212,7 +272,7 @@ test('every possible truncation of an archive is rejected, deterministically', a
212272
213273test ( 'trailing padding after the EOCD is tolerated' , async ( ) => {
214274 // Some streaming writers pad their output to a block size; CPython
215- // tolerates trailing newlines/NULs and so does the pass-2 EOCD scan.
275+ // tolerates trailing newlines/NULs and so does the EOCD scan.
216276 const archive = await buildArchive (
217277 [ await zlib . ZipEntry . create ( 'f.txt' , Buffer . from ( 'hi' ) , { method : 'store' } ) ] ) ;
218278 const padded = Buffer . concat ( [ archive , Buffer . from ( '\r\n\0\0\0' ) ] ) ;
@@ -221,6 +281,15 @@ test('trailing padding after the EOCD is tolerated', async () => {
221281 assert . strictEqual ( ( await entry . content ( ) ) . toString ( ) , 'hi' ) ;
222282} ) ;
223283
284+ test ( 'a maximum-length comment followed by block padding is tolerated' , async ( ) => {
285+ const comment = 'x' . repeat ( 0xffff ) ;
286+ const archive = await buildArchive ( [ ] , comment ) ;
287+ const padded = Buffer . concat ( [ archive , Buffer . alloc ( 4096 ) ] ) ;
288+ const zip = new zlib . ZipBuffer ( padded ) ;
289+
290+ assert . strictEqual ( zip . comment , comment ) ;
291+ } ) ;
292+
224293test ( 'junk appended past a declared comment is tolerated and the comment preserved' , async ( ) => {
225294 const archive = await buildArchive (
226295 [ await zlib . ZipEntry . create ( 'f.txt' , Buffer . from ( 'hi' ) , { method : 'store' } ) ] ,
@@ -262,6 +331,10 @@ test('a record count inconsistent with the directory size is rejected', () => {
262331 const archive = Buffer . concat ( [ Buffer . alloc ( 46 ) , eocd ] ) ;
263332 assert . throws ( ( ) => [ ...zlib . ZipEntry . read ( archive ) ] ,
264333 { code : 'ERR_ZIP_INVALID_ARCHIVE' , message : / i n c o n s i s t e n t / } ) ;
334+
335+ const zeroRecords = Buffer . concat ( [ Buffer . alloc ( 46 ) , buildEocd ( { cdSize : 46 } ) ] ) ;
336+ assert . throws ( ( ) => [ ...zlib . ZipEntry . read ( zeroRecords ) ] ,
337+ { code : 'ERR_ZIP_INVALID_ARCHIVE' , message : / i n c o n s i s t e n t / } ) ;
265338} ) ;
266339
267340test ( 'a corrupted or overrunning central directory header is rejected' , async ( ) => {
0 commit comments