buffer: use stricter range checks

This validates the input to make sure the arguments do not overflow.
Before, if the input would overflow, it would cause the write to be
performt in the wrong spot / result in unexpected behavior.
Instead, just use a strict number validation.

PR-URL: https://github.com/nodejs/node/pull/27045
Fixes: https://github.com/nodejs/node/issues/27043
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
Ruben Bridgewater 2019-04-02 00:01:29 +02:00
parent 2fed83dee8
commit 693401d0dd
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
7 changed files with 98 additions and 102 deletions

View File

@ -1564,12 +1564,6 @@ OpenSSL crypto support.
An attempt was made to use features that require [ICU][], but Node.js was not An attempt was made to use features that require [ICU][], but Node.js was not
compiled with ICU support. compiled with ICU support.
<a id="ERR_NO_LONGER_SUPPORTED"></a>
### ERR_NO_LONGER_SUPPORTED
A Node.js API was called in an unsupported manner, such as
`Buffer.write(string, encoding, offset[, length])`.
<a id="ERR_OUT_OF_RANGE"></a> <a id="ERR_OUT_OF_RANGE"></a>
### ERR_OUT_OF_RANGE ### ERR_OUT_OF_RANGE
@ -2096,6 +2090,12 @@ removed: v10.0.0
Used by the `N-API` when `Constructor.prototype` is not an object. Used by the `N-API` when `Constructor.prototype` is not an object.
<a id="ERR_NO_LONGER_SUPPORTED"></a>
### ERR_NO_LONGER_SUPPORTED
A Node.js API was called in an unsupported manner, such as
`Buffer.write(string, encoding, offset[, length])`.
<a id="ERR_OUTOFMEMORY"></a> <a id="ERR_OUTOFMEMORY"></a>
### ERR_OUTOFMEMORY ### ERR_OUTOFMEMORY
<!-- YAML <!-- YAML

View File

@ -65,17 +65,18 @@ const {
const { const {
codes: { codes: {
ERR_BUFFER_OUT_OF_BOUNDS, ERR_BUFFER_OUT_OF_BOUNDS,
ERR_OUT_OF_RANGE,
ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE, ERR_INVALID_ARG_VALUE,
ERR_INVALID_BUFFER_SIZE, ERR_INVALID_BUFFER_SIZE,
ERR_INVALID_OPT_VALUE, ERR_INVALID_OPT_VALUE,
ERR_NO_LONGER_SUPPORTED,
ERR_UNKNOWN_ENCODING ERR_UNKNOWN_ENCODING
}, },
hideStackFrames hideStackFrames
} = require('internal/errors'); } = require('internal/errors');
const { validateString } = require('internal/validators'); const {
validateInt32,
validateString
} = require('internal/validators');
const { const {
FastBuffer, FastBuffer,
@ -445,7 +446,7 @@ Buffer.concat = function concat(list, length) {
} }
} }
} else { } else {
length = length >>> 0; validateInt32(length, 'length', 0);
} }
const buffer = Buffer.allocUnsafe(length); const buffer = Buffer.allocUnsafe(length);
@ -700,35 +701,27 @@ Buffer.prototype.compare = function compare(target,
if (targetStart === undefined) if (targetStart === undefined)
targetStart = 0; targetStart = 0;
else if (targetStart < 0)
throw new ERR_OUT_OF_RANGE('targetStart', '>= 0', targetStart);
else else
targetStart >>>= 0; validateInt32(targetStart, 'targetStart', 0);
if (targetEnd === undefined) if (targetEnd === undefined)
targetEnd = target.length; targetEnd = target.length;
else if (targetEnd > target.length)
throw new ERR_OUT_OF_RANGE('targetEnd', `<= ${target.length}`, targetEnd);
else else
targetEnd >>>= 0; validateInt32(targetEnd, 'targetEnd', 0, target.length);
if (sourceStart === undefined) if (sourceStart === undefined)
sourceStart = 0; sourceStart = 0;
else if (sourceStart < 0)
throw new ERR_OUT_OF_RANGE('sourceStart', '>= 0', sourceStart);
else else
sourceStart >>>= 0; validateInt32(sourceStart, 'sourceStart', 0);
if (sourceEnd === undefined) if (sourceEnd === undefined)
sourceEnd = this.length; sourceEnd = this.length;
else if (sourceEnd > this.length)
throw new ERR_OUT_OF_RANGE('sourceEnd', `<= ${this.length}`, sourceEnd);
else else
sourceEnd >>>= 0; validateInt32(sourceEnd, 'sourceEnd', 0, this.length);
if (sourceStart >= sourceEnd) if (sourceStart >= sourceEnd)
return (targetStart >= targetEnd ? 0 : -1); return (targetStart >= targetEnd ? 0 : -1);
else if (targetStart >= targetEnd) if (targetStart >= targetEnd)
return 1; return 1;
return compareOffset(this, target, targetStart, sourceStart, targetEnd, return compareOffset(this, target, targetStart, sourceStart, targetEnd,
@ -867,17 +860,13 @@ function _fill(buf, value, offset, end, encoding) {
offset = 0; offset = 0;
end = buf.length; end = buf.length;
} else { } else {
validateInt32(offset, 'offset', 0);
// Invalid ranges are not set to a default, so can range check early. // Invalid ranges are not set to a default, so can range check early.
if (offset < 0)
throw new ERR_OUT_OF_RANGE('offset', '>= 0', offset);
if (end === undefined) { if (end === undefined) {
end = buf.length; end = buf.length;
} else { } else {
if (end > buf.length || end < 0) validateInt32(end, 'end', 0, buf.length);
throw new ERR_OUT_OF_RANGE('end', `>= 0 and <= ${buf.length}`, end);
end = end >>> 0;
} }
offset = offset >>> 0;
if (offset >= end) if (offset >= end)
return buf; return buf;
} }
@ -896,39 +885,37 @@ Buffer.prototype.write = function write(string, offset, length, encoding) {
// Buffer#write(string); // Buffer#write(string);
if (offset === undefined) { if (offset === undefined) {
return this.utf8Write(string, 0, this.length); return this.utf8Write(string, 0, this.length);
}
// Buffer#write(string, encoding) // Buffer#write(string, encoding)
} else if (length === undefined && typeof offset === 'string') { if (length === undefined && typeof offset === 'string') {
encoding = offset; encoding = offset;
length = this.length; length = this.length;
offset = 0; offset = 0;
// Buffer#write(string, offset[, length][, encoding]) // Buffer#write(string, offset[, length][, encoding])
} else if (isFinite(offset)) { } else {
offset = offset >>> 0; if (offset === undefined) {
if (isFinite(length)) { offset = 0;
length = length >>> 0;
} else { } else {
encoding = length; validateInt32(offset, 'offset', 0, this.length);
length = undefined;
} }
const remaining = this.length - offset; const remaining = this.length - offset;
if (length === undefined || length > remaining)
length = remaining;
if (string.length > 0 && (length < 0 || offset < 0)) if (length === undefined) {
throw new ERR_BUFFER_OUT_OF_BOUNDS(); length = remaining;
} else { } else if (typeof length === 'string') {
// If someone is still calling the obsolete form of write(), tell them. encoding = length;
// we don't want eg buf.write("foo", "utf8", 10) to silently turn into length = remaining;
// buf.write("foo", "utf8"), so we can't ignore extra args } else {
throw new ERR_NO_LONGER_SUPPORTED( validateInt32(length, 'length', 0, this.length);
'Buffer.write(string, encoding, offset[, length])' if (length > remaining)
); length = remaining;
}
} }
if (!encoding) return this.utf8Write(string, offset, length); if (!encoding)
return this.utf8Write(string, offset, length);
encoding += ''; encoding += '';
switch (encoding.length) { switch (encoding.length) {

View File

@ -994,7 +994,6 @@ E('ERR_NO_CRYPTO',
'Node.js is not compiled with OpenSSL crypto support', Error); 'Node.js is not compiled with OpenSSL crypto support', Error);
E('ERR_NO_ICU', E('ERR_NO_ICU',
'%s is not supported on Node.js compiled without ICU', TypeError); '%s is not supported on Node.js compiled without ICU', TypeError);
E('ERR_NO_LONGER_SUPPORTED', '%s is no longer supported', Error);
E('ERR_OUT_OF_RANGE', E('ERR_OUT_OF_RANGE',
(str, range, input, replaceDefaultBoolean = false) => { (str, range, input, replaceDefaultBoolean = false) => {
assert(range, 'Missing "range" argument'); assert(range, 'Missing "range" argument');

View File

@ -5,6 +5,12 @@ const vm = require('vm');
const SlowBuffer = require('buffer').SlowBuffer; const SlowBuffer = require('buffer').SlowBuffer;
// Verify the maximum Uint8Array size. There is no concrete limit by spec. The
// internal limits should be updated if this fails.
assert.throws(
() => new Uint8Array(2 ** 31),
{ message: 'Invalid typed array length: 2147483648' }
);
const b = Buffer.allocUnsafe(1024); const b = Buffer.allocUnsafe(1024);
assert.strictEqual(b.length, 1024); assert.strictEqual(b.length, 1024);
@ -59,8 +65,7 @@ assert.throws(() => b.write('test string', 0, 5, 'invalid'),
/Unknown encoding: invalid/); /Unknown encoding: invalid/);
// Unsupported arguments for Buffer.write // Unsupported arguments for Buffer.write
assert.throws(() => b.write('test', 'utf8', 0), assert.throws(() => b.write('test', 'utf8', 0),
/is no longer supported/); { code: 'ERR_INVALID_ARG_TYPE' });
// Try to create 0-length buffers. Should not throw. // Try to create 0-length buffers. Should not throw.
Buffer.from(''); Buffer.from('');
@ -74,27 +79,22 @@ new Buffer('', 'latin1');
new Buffer('', 'binary'); new Buffer('', 'binary');
Buffer(0); Buffer(0);
const outOfBoundsError = {
code: 'ERR_BUFFER_OUT_OF_BOUNDS',
type: RangeError
};
const outOfRangeError = { const outOfRangeError = {
code: 'ERR_OUT_OF_RANGE', code: 'ERR_OUT_OF_RANGE',
type: RangeError type: RangeError
}; };
// Try to write a 0-length string beyond the end of b // Try to write a 0-length string beyond the end of b
common.expectsError(() => b.write('', 2048), outOfBoundsError); common.expectsError(() => b.write('', 2048), outOfRangeError);
// Throw when writing to negative offset // Throw when writing to negative offset
common.expectsError(() => b.write('a', -1), outOfBoundsError); common.expectsError(() => b.write('a', -1), outOfRangeError);
// Throw when writing past bounds from the pool // Throw when writing past bounds from the pool
common.expectsError(() => b.write('a', 2048), outOfBoundsError); common.expectsError(() => b.write('a', 2048), outOfRangeError);
// Throw when writing to negative offset // Throw when writing to negative offset
common.expectsError(() => b.write('a', -1), outOfBoundsError); common.expectsError(() => b.write('a', -1), outOfRangeError);
// Try to copy 0 bytes worth of data into an empty buffer // Try to copy 0 bytes worth of data into an empty buffer
b.copy(Buffer.alloc(0), 0, 0, 0); b.copy(Buffer.alloc(0), 0, 0, 0);
@ -110,8 +110,12 @@ b.copy(Buffer.alloc(1), 0, 2048, 2048);
{ {
const writeTest = Buffer.from('abcdes'); const writeTest = Buffer.from('abcdes');
writeTest.write('n', 'ascii'); writeTest.write('n', 'ascii');
writeTest.write('o', '1', 'ascii'); assert.throws(
writeTest.write('d', '2', 'ascii'); () => writeTest.write('o', '1', 'ascii'),
{ code: 'ERR_INVALID_ARG_TYPE' }
);
writeTest.write('o', 1, 'ascii');
writeTest.write('d', 2, 'ascii');
writeTest.write('e', 3, 'ascii'); writeTest.write('e', 3, 'ascii');
writeTest.write('j', 4, 'ascii'); writeTest.write('j', 4, 'ascii');
assert.strictEqual(writeTest.toString(), 'nodejs'); assert.strictEqual(writeTest.toString(), 'nodejs');

View File

@ -10,7 +10,7 @@ assert.strictEqual(a.compare(b), -1);
// Equivalent to a.compare(b). // Equivalent to a.compare(b).
assert.strictEqual(a.compare(b, 0), -1); assert.strictEqual(a.compare(b, 0), -1);
assert.strictEqual(a.compare(b, '0'), -1); assert.throws(() => a.compare(b, '0'), { code: 'ERR_INVALID_ARG_TYPE' });
assert.strictEqual(a.compare(b, undefined), -1); assert.strictEqual(a.compare(b, undefined), -1);
// Equivalent to a.compare(b). // Equivalent to a.compare(b).
@ -18,7 +18,10 @@ assert.strictEqual(a.compare(b, 0, undefined, 0), -1);
// Zero-length target, return 1 // Zero-length target, return 1
assert.strictEqual(a.compare(b, 0, 0, 0), 1); assert.strictEqual(a.compare(b, 0, 0, 0), 1);
assert.strictEqual(a.compare(b, '0', '0', '0'), 1); assert.throws(
() => a.compare(b, 0, '0', '0'),
{ code: 'ERR_INVALID_ARG_TYPE' }
);
// Equivalent to Buffer.compare(a, b.slice(6, 10)) // Equivalent to Buffer.compare(a, b.slice(6, 10))
assert.strictEqual(a.compare(b, 6, 10), 1); assert.strictEqual(a.compare(b, 6, 10), 1);
@ -45,24 +48,41 @@ assert.strictEqual(a.compare(b, 0, 7, 4), -1);
// Equivalent to Buffer.compare(a.slice(4, 6), b.slice(0, 7)); // Equivalent to Buffer.compare(a.slice(4, 6), b.slice(0, 7));
assert.strictEqual(a.compare(b, 0, 7, 4, 6), -1); assert.strictEqual(a.compare(b, 0, 7, 4, 6), -1);
// zero length target // Null is ambiguous.
assert.strictEqual(a.compare(b, 0, null), 1); assert.throws(
() => a.compare(b, 0, null),
{ code: 'ERR_INVALID_ARG_TYPE' }
);
// Coerces to targetEnd == 5 // Values do not get coerced.
assert.strictEqual(a.compare(b, 0, { valueOf: () => 5 }), -1); assert.throws(
() => a.compare(b, 0, { valueOf: () => 5 }),
{ code: 'ERR_INVALID_ARG_TYPE' }
);
// zero length target // Infinity should not be coerced.
assert.strictEqual(a.compare(b, Infinity, -Infinity), 1); assert.throws(
() => a.compare(b, Infinity, -Infinity),
{ code: 'ERR_OUT_OF_RANGE' }
);
// Zero length target because default for targetEnd <= targetSource // Zero length target because default for targetEnd <= targetSource
assert.strictEqual(a.compare(b, '0xff'), 1); assert.strictEqual(a.compare(b, 0xff), 1);
const oor = common.expectsError({ code: 'ERR_OUT_OF_RANGE' }, 7); assert.throws(
() => a.compare(b, '0xff'),
{ code: 'ERR_INVALID_ARG_TYPE' }
);
assert.throws(
() => a.compare(b, 0, '0xff'),
{ code: 'ERR_INVALID_ARG_TYPE' }
);
const oor = { code: 'ERR_OUT_OF_RANGE' };
assert.throws(() => a.compare(b, 0, 100, 0), oor); assert.throws(() => a.compare(b, 0, 100, 0), oor);
assert.throws(() => a.compare(b, 0, 1, 0, 100), oor); assert.throws(() => a.compare(b, 0, 1, 0, 100), oor);
assert.throws(() => a.compare(b, -1), oor); assert.throws(() => a.compare(b, -1), oor);
assert.throws(() => a.compare(b, 0, '0xff'), oor);
assert.throws(() => a.compare(b, 0, Infinity), oor); assert.throws(() => a.compare(b, 0, Infinity), oor);
assert.throws(() => a.compare(b, 0, 1, -1), oor); assert.throws(() => a.compare(b, 0, 1, -1), oor);
assert.throws(() => a.compare(b, -Infinity, Infinity), oor); assert.throws(() => a.compare(b, -Infinity, Infinity), oor);

View File

@ -333,34 +333,17 @@ assert.strictEqual(
// Make sure "end" is properly checked, even if it's magically mangled using // Make sure "end" is properly checked, even if it's magically mangled using
// Symbol.toPrimitive. // Symbol.toPrimitive.
{ {
let elseWasLast = false;
const expectedErrorMessage =
'The value of "end" is out of range. It must be >= 0 and <= 1. Received -1';
common.expectsError(() => { common.expectsError(() => {
let ctr = 0;
const end = { const end = {
[Symbol.toPrimitive]() { [Symbol.toPrimitive]() {
// We use this condition to get around the check in lib/buffer.js return 1;
if (ctr === 0) {
elseWasLast = false;
ctr++;
return 1;
}
elseWasLast = true;
// Once buffer.js calls the C++ implementation of fill, return -1
return -1;
} }
}; };
Buffer.alloc(1).fill(Buffer.alloc(1), 0, end); Buffer.alloc(1).fill(Buffer.alloc(1), 0, end);
}, { }, {
code: 'ERR_OUT_OF_RANGE', code: 'ERR_INVALID_ARG_TYPE',
type: RangeError, message: 'The "end" argument must be of type number. Received type object'
message: expectedErrorMessage
}); });
// Make sure -1 is making it to Buffer::Fill().
assert.ok(elseWasLast,
'internal API changed, -1 no longer in correct location');
} }
// Testing process.binding. Make sure "end" is properly checked for -1 wrap // Testing process.binding. Make sure "end" is properly checked for -1 wrap

View File

@ -3,14 +3,17 @@
const common = require('../common'); const common = require('../common');
const assert = require('assert'); const assert = require('assert');
const outsideBounds = common.expectsError({ [-1, 10].forEach((offset) => {
code: 'ERR_BUFFER_OUT_OF_BOUNDS', assert.throws(
type: RangeError, () => Buffer.alloc(9).write('foo', offset),
message: 'Attempt to write outside buffer bounds' {
}, 2); code: 'ERR_OUT_OF_RANGE',
name: 'RangeError',
assert.throws(() => Buffer.alloc(9).write('foo', -1), outsideBounds); message: 'The value of "offset" is out of range. ' +
assert.throws(() => Buffer.alloc(9).write('foo', 10), outsideBounds); `It must be >= 0 && <= 9. Received ${offset}`
}
);
});
const resultMap = new Map([ const resultMap = new Map([
['utf8', Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])], ['utf8', Buffer.from([102, 111, 111, 0, 0, 0, 0, 0, 0])],