Skip to content

Commit 3a6fe06

Browse files
authored
Fix crash when using fromBuffer() to read corrupt zip files that specify out of bounds file offsets (#157)
2 parents 6fbfef9 + 49840a2 commit 3a6fe06

File tree

3 files changed

+119
-75
lines changed

3 files changed

+119
-75
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,9 @@ The zip file specification has several ambiguities inherent in its design. Yikes
763763

764764
## Change History
765765

766+
* 3.1.3
767+
* Fixed a crash when using `fromBuffer()` to read corrupt zip files that specify out of bounds file offsets. [issue #156](https://github.com/thejoshwolfe/yauzl/pull/156)
768+
* Enahnced the test suite to run the error tests through `fromBuffer()` and `fromRandomAccessReader()` in addition to `open()`, which would have caught the above.
766769
* 3.1.2
767770
* Fixed handling non-64 bit entries (similar to the version 3.1.1 fix) that actually have exactly 0xffffffff values in the fields. This fixes erroneous "expected zip64 extended information extra field" errors. [issue #109](https://github.com/thejoshwolfe/yauzl/pull/109)
768771
* 3.1.1
@@ -836,3 +839,11 @@ The zip file specification has several ambiguities inherent in its design. Yikes
836839
* Fix bug with using `iconv`.
837840
* 2.0.0
838841
* Initial release.
842+
843+
## Development
844+
845+
One of the trickiest things in development is crafting test cases located in `test/{success,failure}/`.
846+
These are zip files that have been specifically generated or design to test certain conditions in this library.
847+
I recommend using [hexdump-zip](https://github.com/thejoshwolfe/hexdump-zip) to examine the structure of a zipfile.
848+
849+
For making new error cases, I typically start by copying `test/success/linux-info-zip.zip`, and then editing a few bytes with a hex editor.

fd-slicer.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,29 @@ function BufferSlicer(buffer, options) {
195195
}
196196

197197
BufferSlicer.prototype.read = function(buffer, offset, length, position, callback) {
198-
var end = position + length;
199-
var delta = end - this.buffer.length;
200-
var written = (delta > 0) ? delta : length;
201-
this.buffer.copy(buffer, offset, position, end);
198+
if (!(0 <= offset && offset <= buffer.length)) throw new RangeError("offset outside buffer: 0 <= " + offset + " <= " + buffer.length);
199+
if (position < 0) throw new RangeError("position is negative: " + position);
200+
if (offset + length > buffer.length) {
201+
// The caller's buffer can't hold all the bytes they're trying to read.
202+
// Clamp the length instead of giving an error.
203+
// The callback will be informed of fewer than expected bytes written.
204+
length = buffer.length - offset;
205+
}
206+
if (position + length > this.buffer.length) {
207+
// Clamp any attempt to read past the end of the source buffer.
208+
length = this.buffer.length - position;
209+
}
210+
if (length <= 0) {
211+
// After any clamping, we're fully out of bounds or otherwise have nothing to do.
212+
// This isn't an error; it's just zero bytes written.
213+
setImmediate(function() {
214+
callback(null, 0);
215+
});
216+
return;
217+
}
218+
this.buffer.copy(buffer, offset, position, position + length);
202219
setImmediate(function() {
203-
callback(null, written);
220+
callback(null, length);
204221
});
205222
};
206223

test/test.js

+86-70
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ function shouldDoTest(testPath) {
2525
return args.indexOf(testPath) !== -1;
2626
}
2727

28+
var openFunctions = [
29+
function(zipfilePath, testId, options, callback) { yauzl.open(zipfilePath, options, callback); },
30+
function(zipfilePath, testId, options, callback) { yauzl.fromBuffer(fs.readFileSync(zipfilePath), options, callback); },
31+
function(zipfilePath, testId, options, callback) { openWithRandomAccess(zipfilePath, options, true, testId, callback); },
32+
function(zipfilePath, testId, options, callback) { openWithRandomAccess(zipfilePath, options, false, testId, callback); },
33+
];
34+
var openFunctionNames = [
35+
"fd",
36+
"buffer",
37+
"randomAccess",
38+
"minimalRandomAccess",
39+
];
40+
2841
// success tests
2942
listZipFiles([path.join(__dirname, "success"), path.join(__dirname, "wrong-entry-sizes")]).forEach(function(zipfilePath) {
3043
if (!shouldDoTest(zipfilePath)) return;
@@ -38,15 +51,9 @@ listZipFiles([path.join(__dirname, "success"), path.join(__dirname, "wrong-entry
3851
options.validateEntrySizes = false;
3952
});
4053
}
41-
var openFunctions = [
42-
function(testId, options, callback) { yauzl.open(zipfilePath, options, callback); },
43-
function(testId, options, callback) { yauzl.fromBuffer(fs.readFileSync(zipfilePath), options, callback); },
44-
function(testId, options, callback) { openWithRandomAccess(zipfilePath, options, true, testId, callback); },
45-
function(testId, options, callback) { openWithRandomAccess(zipfilePath, options, false, testId, callback); },
46-
];
4754
openFunctions.forEach(function(openFunction, i) {
4855
optionConfigurations.forEach(function(options, j) {
49-
var testId = zipfilePath + "(" + ["fd", "buffer", "randomAccess", "minimalRandomAccess"][i] + "," + j + "): ";
56+
var testId = zipfilePath + "(" + openFunctionNames[i] + "," + j + "): ";
5057
var expectedPathPrefix = zipfilePath.replace(/\.zip$/, "");
5158
var expectedArchiveContents = {};
5259
var DIRECTORY = 1; // not a string
@@ -77,7 +84,7 @@ listZipFiles([path.join(__dirname, "success"), path.join(__dirname, "wrong-entry
7784
}
7885
}
7986
pend.go(function(zipfileCallback) {
80-
openFunction(testId, options, function(err, zipfile) {
87+
openFunction(zipfilePath, testId, options, function(err, zipfile) {
8188
if (err) throw err;
8289
zipfile.readEntry();
8390
zipfile.on("entry", function(entry) {
@@ -163,68 +170,77 @@ listZipFiles([path.join(__dirname, "success"), path.join(__dirname, "wrong-entry
163170
listZipFiles([path.join(__dirname, "failure")]).forEach(function(zipfilePath) {
164171
if (!shouldDoTest(zipfilePath)) return;
165172
var expectedErrorMessage = path.basename(zipfilePath).replace(/(_\d+)?\.zip$/, "");
166-
var failedYet = false;
167-
var emittedError = false;
168-
pend.go(function(cb) {
169-
var operationsInProgress = 0;
170-
if (/invalid characters in fileName/.test(zipfilePath)) {
171-
// this error can only happen when you specify an option
172-
yauzl.open(zipfilePath, {strictFileNames: true}, onZipFile);
173-
} else {
174-
yauzl.open(zipfilePath, onZipFile);
175-
}
176-
return;
177-
178-
function onZipFile(err, zipfile) {
179-
if (err) return checkErrorMessage(err);
180-
zipfile.on("error", function(err) {
181-
noEventsAllowedAfterError();
182-
emittedError = true;
183-
checkErrorMessage(err);
184-
});
185-
zipfile.on("entry", function(entry) {
186-
noEventsAllowedAfterError();
187-
// let's also try to read directories, cuz whatever.
188-
operationsInProgress += 1;
189-
zipfile.openReadStream(entry, function(err, stream) {
190-
if (err) return checkErrorMessage(err);
191-
stream.on("error", function(err) {
192-
checkErrorMessage(err);
193-
});
194-
stream.on("data", function(data) {
195-
// ignore
196-
});
197-
stream.on("end", function() {
198-
doneWithSomething();
173+
174+
openFunctions.forEach(function(openFunction, i) {
175+
var testId = zipfilePath + "(" + openFunctionNames[i] + "): ";
176+
var failedYet = false;
177+
var emittedError = false;
178+
pend.go(function(cb) {
179+
var operationsInProgress = 0;
180+
var options = null;
181+
if (/invalid characters in fileName/.test(zipfilePath)) {
182+
// this error can only happen when you specify an option
183+
options = {strictFileNames: true};
184+
}
185+
openFunction(zipfilePath, testId, options, onZipFile);
186+
187+
function onZipFile(err, zipfile) {
188+
if (err) return checkErrorMessage(err);
189+
zipfile.on("error", function(err) {
190+
noEventsAllowedAfterError();
191+
emittedError = true;
192+
checkErrorMessage(err);
193+
});
194+
zipfile.on("entry", function(entry) {
195+
noEventsAllowedAfterError();
196+
// let's also try to read directories, cuz whatever.
197+
operationsInProgress += 1;
198+
zipfile.openReadStream(entry, function(err, stream) {
199+
if (err) return checkErrorMessage(err);
200+
stream.on("error", function(err) {
201+
checkErrorMessage(err);
202+
});
203+
stream.on("data", function(data) {
204+
// ignore
205+
});
206+
stream.on("end", function() {
207+
doneWithSomething();
208+
});
199209
});
200210
});
201-
});
202-
operationsInProgress += 1;
203-
zipfile.on("end", function() {
204-
noEventsAllowedAfterError();
205-
doneWithSomething();
206-
});
207-
function doneWithSomething() {
208-
operationsInProgress -= 1;
209-
if (operationsInProgress !== 0) return;
210-
if (!failedYet) {
211-
throw new Error(zipfilePath + ": expected failure");
211+
operationsInProgress += 1;
212+
zipfile.on("end", function() {
213+
noEventsAllowedAfterError();
214+
doneWithSomething();
215+
});
216+
function doneWithSomething() {
217+
operationsInProgress -= 1;
218+
if (operationsInProgress !== 0) return;
219+
if (!failedYet) {
220+
throw new Error(testId + "expected failure");
221+
}
212222
}
213223
}
214-
}
215-
function checkErrorMessage(err) {
216-
var actualMessage = err.message.replace(/[^0-9A-Za-z-]+/g, " ").trimRight();
217-
if (actualMessage !== expectedErrorMessage) {
218-
throw new Error(zipfilePath + ": wrong error message: " + actualMessage);
224+
function checkErrorMessage(err) {
225+
var actualMessage = err.message.replace(/[^0-9A-Za-z-]+/g, " ").trimRight();
226+
if (actualMessage !== expectedErrorMessage) {
227+
if (i !== 0) {
228+
// The error messages are tuned for the common case.
229+
// Sometimes other open functions give slightly different error messages, and that's ok,
230+
// as long as we're still getting some error.
231+
} else {
232+
throw new Error(testId + "wrong error message: " + actualMessage);
233+
}
234+
}
235+
console.log(testId + "pass");
236+
failedYet = true;
237+
operationsInProgress = -Infinity;
238+
cb();
219239
}
220-
console.log(zipfilePath + ": pass");
221-
failedYet = true;
222-
operationsInProgress = -Infinity;
223-
cb();
224-
}
225-
function noEventsAllowedAfterError() {
226-
if (emittedError) throw new Error("events emitted after error event");
227-
}
240+
function noEventsAllowedAfterError() {
241+
if (emittedError) throw new Error(testId + "events emitted after error event");
242+
}
243+
});
228244
});
229245
});
230246

@@ -471,11 +487,11 @@ function openWithRandomAccess(zipfilePath, options, implementRead, testId, callb
471487
if (implementRead) {
472488
InefficientRandomAccessReader.prototype.read = function(buffer, offset, length, position, callback) {
473489
fs.open(zipfilePath, "r", function(err, fd) {
474-
if (err) throw err;
490+
if (err) return callback(err);
475491
fs.read(fd, buffer, offset, length, position, function(err, bytesRead) {
476-
if (bytesRead < length) throw new Error("unexpected EOF");
492+
if (bytesRead < length) return callback(new Error("unexpected EOF"));
477493
fs.close(fd, function(err) {
478-
if (err) throw err;
494+
if (err) return callback(err);
479495
callback();
480496
});
481497
});
@@ -488,10 +504,10 @@ function openWithRandomAccess(zipfilePath, options, implementRead, testId, callb
488504
};
489505

490506
fs.stat(zipfilePath, function(err, stats) {
491-
if (err) throw err;
507+
if (err) return callback(err);
492508
var reader = new InefficientRandomAccessReader();
493509
yauzl.fromRandomAccessReader(reader, stats.size, options, function(err, zipfile) {
494-
if (err) throw err;
510+
if (err) return callback(err);
495511
callback(null, zipfile);
496512
});
497513
});

0 commit comments

Comments
 (0)