mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-12 01:55:55 -07:00
fix(dictionary): reject zips with corrupted central directory records
- Walk every central-directory record and validate its signature and variable-length field bounds so an in-place corrupted record is rejected even when the archive's overall size still matches - Add tests covering a corrupted record signature and an out-of-bounds name length
This commit is contained in:
@@ -143,6 +143,21 @@ test('readDictionaryZipRevision reads the built revision and rejects foreign arc
|
|||||||
assert.equal(readDictionaryZipRevision(cutPath), null, `cut of ${missingBytes} bytes`);
|
assert.equal(readDictionaryZipRevision(cutPath), null, `cut of ${missingBytes} bytes`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same size, corrupt directory: a record overwritten in place has to be rejected too.
|
||||||
|
const centralStart = archive.readUInt32LE(archive.length - 22 + 16);
|
||||||
|
const brokenSignaturePath = path.join(dir, 'broken-signature.zip');
|
||||||
|
const brokenSignature = Buffer.from(archive);
|
||||||
|
brokenSignature.writeUInt32LE(0xdeadbeef, centralStart);
|
||||||
|
fs.writeFileSync(brokenSignaturePath, brokenSignature);
|
||||||
|
assert.equal(readDictionaryZipRevision(brokenSignaturePath), null);
|
||||||
|
|
||||||
|
const brokenLengthPath = path.join(dir, 'broken-length.zip');
|
||||||
|
const brokenLength = Buffer.from(archive);
|
||||||
|
// Name length that runs the walk past the end of the directory.
|
||||||
|
brokenLength.writeUInt16LE(0xffff, centralStart + 28);
|
||||||
|
fs.writeFileSync(brokenLengthPath, brokenLength);
|
||||||
|
assert.equal(readDictionaryZipRevision(brokenLengthPath), null);
|
||||||
|
|
||||||
const foreignPath = path.join(dir, 'foreign.zip');
|
const foreignPath = path.join(dir, 'foreign.zip');
|
||||||
fs.writeFileSync(foreignPath, Buffer.from('not a zip at all', 'utf8'));
|
fs.writeFileSync(foreignPath, Buffer.from('not a zip at all', 'utf8'));
|
||||||
assert.equal(readDictionaryZipRevision(foreignPath), null);
|
assert.equal(readDictionaryZipRevision(foreignPath), null);
|
||||||
|
|||||||
@@ -152,6 +152,50 @@ const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
|
|||||||
const LOCAL_FILE_HEADER_SIZE = 30;
|
const LOCAL_FILE_HEADER_SIZE = 30;
|
||||||
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
|
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
|
||||||
const END_OF_CENTRAL_DIRECTORY_SIZE = 22;
|
const END_OF_CENTRAL_DIRECTORY_SIZE = 22;
|
||||||
|
const CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
|
||||||
|
const CENTRAL_FILE_HEADER_SIZE = 46;
|
||||||
|
// 65535 entries with names of a few dozen bytes stay far under this; the cap only stops a corrupt
|
||||||
|
// record length from asking for an allocation the size of the archive.
|
||||||
|
const MAX_CENTRAL_DIRECTORY_BYTES = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks every declared central-directory record, checking each signature and keeping the
|
||||||
|
* variable-length name/extra/comment fields inside the directory. The walk has to land exactly on
|
||||||
|
* the end of the directory, so a record that was overwritten in place fails even though the file
|
||||||
|
* kept its size.
|
||||||
|
*/
|
||||||
|
function isCentralDirectoryIntact(
|
||||||
|
fd: number,
|
||||||
|
centralStart: number,
|
||||||
|
centralSize: number,
|
||||||
|
entryCount: number,
|
||||||
|
): boolean {
|
||||||
|
if (centralSize === 0 || centralSize > MAX_CENTRAL_DIRECTORY_BYTES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const central = Buffer.alloc(centralSize);
|
||||||
|
if (fs.readSync(fd, central, 0, centralSize, centralStart) !== centralSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor = 0;
|
||||||
|
for (let index = 0; index < entryCount; index += 1) {
|
||||||
|
if (cursor + CENTRAL_FILE_HEADER_SIZE > centralSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (central.readUInt32LE(cursor) !== CENTRAL_FILE_HEADER_SIGNATURE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nameLength = central.readUInt16LE(cursor + 28);
|
||||||
|
const extraLength = central.readUInt16LE(cursor + 30);
|
||||||
|
const commentLength = central.readUInt16LE(cursor + 32);
|
||||||
|
cursor += CENTRAL_FILE_HEADER_SIZE + nameLength + extraLength + commentLength;
|
||||||
|
if (cursor > centralSize) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cursor === centralSize;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start of the central directory, or null when the archive is not a complete one of ours. The
|
* Start of the central directory, or null when the archive is not a complete one of ours. The
|
||||||
@@ -171,7 +215,8 @@ function readCentralDirectoryStart(fd: number, fileSize: number): number | null
|
|||||||
if (end.readUInt32LE(0) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE || end.readUInt16LE(20) !== 0) {
|
if (end.readUInt32LE(0) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE || end.readUInt16LE(20) !== 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (end.readUInt16LE(10) === 0) {
|
const entryCount = end.readUInt16LE(10);
|
||||||
|
if (entryCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const centralSize = end.readUInt32LE(12);
|
const centralSize = end.readUInt32LE(12);
|
||||||
@@ -179,6 +224,9 @@ function readCentralDirectoryStart(fd: number, fileSize: number): number | null
|
|||||||
if (centralStart + centralSize !== endOffset) {
|
if (centralStart + centralSize !== endOffset) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (!isCentralDirectoryIntact(fd, centralStart, centralSize, entryCount)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return centralStart;
|
return centralStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user