Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

buffer: add Buffer.harden() method #28439

Closed
wants to merge 9 commits into from
48 changes: 48 additions & 0 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,54 @@ const buf = Buffer.from(new Foo(), 'utf8');
A `TypeError` will be thrown if `object` has not mentioned methods or is not of
other type appropriate for `Buffer.from()` variants.

### Class Method: Buffer.harden([options])
<!-- YAML
added: TODO
-->

> Stability: 1 - Experimental

* `options` {Object}
* `zeroFill` {boolean}
If `true`, all future `Buffer` instances, even the ones created with
`Buffer.allocUnsafe`, will be zero-filled by default.
**Default:** `true`.
* `disablePool` {boolean}
If `true`, all future `Buffer` allocations will not be pooled and their
underlying `ArrayBuffer` will have the same size as `Buffer` instance.
**Default:** `true`.
* `throwOnUnsafe` {boolean}
If `true`, unsafe `Buffer(arg)` API will throw instead of showing a
deprecation warning.
Otherwise, if `false`, first call to unsafe `Buffer(arg)` will trigger a
deprecation warning, regardless of whether it was called from inside of
`node_modules` directory or not (unlike the default warning).
**Default:** `true`.
* `freeze` {boolean}
If `true`, `Buffer` and `require('buffer')` objects are frozen and can
not be monkey-patched.
`Buffer.prototype` is not frozen for compatibility reasons.
**Default:** `true`.

Calling `Buffer.harden()` enables additional security measures for Buffer API,
disabling some trade-offs between security and performance/compatibility that
are present by default.

This method has effect only of subsequent Buffer API usage, Buffer instances
created before `Buffer.harden()` is called are not affected.

Warning: for ecosystem compatibility and security reasons `Buffer.harden()` can
be called only once and only from the top-level application code and only before
the first turn of the event loop. Attempting to call it asynchronously in
runtime or from a library in `node_modules` will throw.

By default, it enables mandratory zero fill, disables Buffer pooling, disables
deprecated unsafe Buffer API, freezes `Buffer` and `require('buffer')` objects.

The exact behaviour could be fine-tuned: for example, to still allow unsafe
Buffer API for compatibility reasons (e.g. if that us required by application
dependencies), `Buffer.harden({ throwOnUnsafe: false })` could be used.

### Class Method: Buffer.from(string[, encoding])
<!-- YAML
added: v5.10.0
Expand Down
71 changes: 69 additions & 2 deletions lib/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const {

const {
codes: {
ERR_ASSERTION,
ERR_BUFFER_OUT_OF_BOUNDS,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
Expand Down Expand Up @@ -109,8 +110,18 @@ let poolSize, poolOffset, allocPool;
// do not own the ArrayBuffer allocator. Zero fill is always on in that case.
const zeroFill = bindingZeroFill || [0];

// Hardening Buffer enables disables unsafe Buffer API, disables pooling,
// and enables mandratory zero-fill. This is more secure, but has potential
// performance and compatibility impact, depending on the usecase.
const hardened = {
applied: false,
zeroFill: false,
disablePool: false,
throwOnUnsafe: false,
};

function createUnsafeBuffer(size) {
zeroFill[0] = 0;
zeroFill[0] = hardened.zeroFill ? 1 : 0;
try {
return new FastBuffer(size);
} finally {
Expand Down Expand Up @@ -140,6 +151,18 @@ const bufferWarning = 'Buffer() is deprecated due to security and usability ' +
'Buffer.allocUnsafe(), or Buffer.from() methods instead.';

function showFlaggedDeprecation() {
if (hardened.applied) {
if (hardened.throwOnUnsafe) {
throw new ERR_ASSERTION(
'Unsafe Buffer() API is forbidden by Buffer strict hardening opt-in.'
);
}
if (bufferWarningAlreadyEmitted) return;
process.emitWarning(bufferWarning, 'DeprecationWarning', 'DEP0005');
bufferWarningAlreadyEmitted = true;
ChALkeR marked this conversation as resolved.
Show resolved Hide resolved
return;
}

if (bufferWarningAlreadyEmitted ||
++nodeModulesCheckCounter > 10000 ||
(!require('internal/options').getOptionValue('--pending-deprecation') &&
Expand All @@ -157,6 +180,50 @@ function showFlaggedDeprecation() {
bufferWarningAlreadyEmitted = true;
}

// Calling this method does not affect existing buffers, only new ones.
Buffer.harden = function({
zeroFill = true,
disablePool = true,
throwOnUnsafe = true,
freeze = true,
} = {}) {
if (hardened.applied) {
// So that params are not changed afterwards
throw new ERR_ASSERTION(
'Buffer.harden can be called only once'
);
}
if (isInsideNodeModules()) {
throw new ERR_ASSERTION(
'Buffer.harden() should be called only from the top-level module. ' +
ChALkeR marked this conversation as resolved.
Show resolved Hide resolved
'Calling Buffer.harden() from dependencies is not supported.'
);
}
const perf_hooks = require('perf_hooks');
const stillSynchronous = perf_hooks.performance.nodeTiming.loopStart < 0;
if (!stillSynchronous) throw new ERR_ASSERTION(
'Buffer.harden() should be called only in synchronous mode, during app ' +
'startup. Calling Buffer.harden() asynchronously is not supported.'
);
Object.assign(hardened, {
applied: true,
zeroFill: Boolean(zeroFill),
disablePool: Boolean(disablePool),
throwOnUnsafe: Boolean(throwOnUnsafe),
});
if (disablePool) {
Object.defineProperty(Buffer, 'poolSize', {
enumerable: true,
get: () => 0,
set: () => {},
});
}
if (freeze) {
Object.freeze(Buffer);
Object.freeze(module.exports);
}
};

function toInteger(n, defaultVal) {
n = +n;
if (!Number.isNaN(n) &&
Expand Down Expand Up @@ -365,7 +432,7 @@ function allocate(size) {
if (size <= 0) {
return new FastBuffer();
}
if (size < (Buffer.poolSize >>> 1)) {
if (size < (Buffer.poolSize >>> 1) && !hardened.disablePool) {
if (size > (poolSize - poolOffset))
createPool();
const b = new FastBuffer(allocPool, poolOffset, size);
Expand Down