Skip to main content
interlace
Plugin: node-securityRules

no-unsafe-buffer-alloc

Disallow `Buffer.allocUnsafe()` and `Buffer.allocUnsafeSlow()`, which return uninitialized memory.

Disallow Buffer.allocUnsafe() and Buffer.allocUnsafeSlow(). Both hand back a buffer over memory that was never zeroed — until every byte is overwritten, the buffer contains whatever the allocator last stored there.

This rule is part of eslint-plugin-node-security.

Quick Summary

AspectDetails
SeverityHigh (Security — memory disclosure)
Auto-Fix💡 Suggestion (Buffer.alloc)
CategoryNode Security
CWECWE-908
Recommendedwarn — reports unless coverage is structurally visible (see below)
Best ForCodebases that send, hash, or persist buffers built by hand

What the rule reports, and what it lets through

The hazard is a byte that is read before it is written. A buffer that is filled before anything looks at it discloses nothing, so the rule stays silent on two shapes and reports everything else.

1. Zeroed in the same expression. A parent-node check, not variable tracking.

Buffer.allocUnsafe(64).fill(0); // not reported — equivalent to Buffer.alloc(64)

2. Covered before it is read. The allocation is bound to a local, and the first thing that touches that local is a covering write.

// Not reported — the loop writes every byte before `result` escapes.
// (redis/ioredis lib/Command.ts:667)
const result = Buffer.allocUnsafe(this.length);
let offset = 0;
for (const item of this.items) {
  const length = Buffer.byteLength(item);
  Buffer.isBuffer(item)
    ? item.copy(result, offset)
    : result.write(item, offset, length);
  offset += length;
}
return result;

"Covering" is narrower than "any write", because proving full coverage is undecidable and the rule refuses to pretend otherwise. Two shapes cover by construction:

ShapeCovering?Why
buf.fill(v)fills to the end of the buffer by definition
src.copy(buf), buf.set(src), buf.write(s)one argument: starts at 0, runs for the source's whole length
buf.write(s, offset) / src.copy(buf, offset) inside a loopa moving offset is a buffer being walked
buf.writeUInt32BE(v, 0)a fixed offset stamps one field and leaves the rest
src.copy(buf, 0, 0, 4)a fixed slice
buf[i] = vone byte

A partial write does not end the search — it is not a read either — so a fixed-offset write followed by a covering one is still accepted. A read reached before any covering write is reported.

Anything the rule cannot resolve to a local binding still reports: return Buffer.allocUnsafe(n), this.buf = Buffer.allocUnsafe(n), f(Buffer.allocUnsafe(n)). References are compared in source order, which is an approximation of execution order: a buffer written inside a callback declared above its first read is judged on where the text sits, not on when it runs.

For the complementary read-side analysis — buffers read past their written region (CWE-126) — see no-buffer-overread.

The CWE-789 half: allocation sized off the wire

The same rule reports new Array(n), Buffer.alloc(n) and the typed-array constructors when n is a length field a peer chose. The mechanism is not the one this documentation used to claim ("a large length makes the process allocate and then walk a huge structure"). Measured on V8 (node 24, --expose-gc, heapUsed delta):

new Array(1e9)  ->  0.003 ms,    0.0 MB   // dictionary mode — free
new Array(3e7)  -> 20.700 ms,  228.9 MB   // packed elements — 12 wire bytes buy 229 MB
new Array(4e7)  ->  0.007 ms,    0.0 MB   // flips back to dictionary mode

The cost is a narrow band below V8's packed-elements limit (~33.5M elements), not a monotonic function of n. That inverts the usual advice: a bounds check that rejects only implausibly large lengths rejects exactly the values that cost nothing, and admits the ones that cost a quarter of a gigabyte. Clamp against the maximum the protocol permits, not against a number that merely looks big.

Buffer.alloc(n) and the typed arrays have no such threshold — they commit n bytes at every n.

Why allocUnsafe is dangerous

Buffer.alloc(size) zero-fills. Buffer.allocUnsafe(size) skips that step and returns a slice of a pre-allocated pool, which is why it is faster. The bytes it returns are whatever the previous owner of that memory left behind: decrypted request bodies, session tokens, private keys.

Any byte not overwritten before the buffer is read, hashed, written to disk, or sent over the network discloses that memory to whoever receives it. This is the same class of defect as CVE-2018-7166, where Node's own randomFillSync fallback returned unzeroed memory.

Buffer.allocUnsafeSlow(size) has the identical hazard — it only differs in allocating outside the shared pool.

AllocatorZero-filledReported by this rule
Buffer.alloc(size)✅ yesno
Buffer.allocUnsafe(size)❌ noyes
Buffer.allocUnsafeSlow(size)❌ noyes
Buffer.allocUnsafe(size).fill(0)✅ yesno (structural exemption)
const b = Buffer.allocUnsafe(n); src.copy(b);✅ effectivelyno (covered before read)

Examples

❌ Incorrect

const buf = Buffer.allocUnsafe(1024);
res.end(buf);                          // leaks prior heap contents

const header = Buffer.allocUnsafe(16);
header.writeUInt32BE(len, 0);          // 12 bytes still uninitialized
socket.write(header);                  // …and they go out on the wire

const slow = Buffer.allocUnsafeSlow(64);

✅ Correct

const buf = Buffer.alloc(1024);              // zero-filled
const header = Buffer.alloc(16);
header.writeUInt32BE(len, 0);                // remaining bytes are zeros

const scratch = Buffer.allocUnsafe(64).fill(0); // zeroed at the call site

const copy = Buffer.allocUnsafe(src.length);    // covered before any read
src.copy(copy);
send(copy);

Options

OptionTypeDefaultDescription
wireNamesstring[]["chunk","chunks","buffer","buf","payload","frame","packet","raw","message","msg"]Binding names that carry bytes off the wire. Replaces the default; pass [] to turn the name arm off.
requestRootNamesstring[]["req","request","ctx","event"]Root identifiers treated as a request. Replaces the default.
countNamesstring[]["length","len","size","count","n","num","total","capacity","bytelength"]Identifiers that hold an allocation size rather than a payload. Replaces the default.

Error Message Format

🔒 NODE-SECURITY CWE-908 | Uninitialized Buffer Allocation | HIGH
   Fix: Use `Buffer.alloc(size)` (zero-filled), or keep `allocUnsafe` only where
   the very next statement overwrites the whole buffer.

When Not To Use It

Turn the rule off (or scope it with an override) in code where allocUnsafe is a measured, reviewed performance decision and coverage is proved by an argument the rule cannot see — a pool whose bookkeeping lives in another module, or a codec that hands the buffer to native code to fill.

Known False Positives

  • An allocation covered through a shape the rule does not model: filled by a helper (fillHeader(buf)), by a native binding, or across a module boundary. These stay reported; unresolved is not the same as safe.
  • Coverage proved only by execution order that differs from source order — a buffer written inside a callback declared above its first read.

Known False Negatives

  • Computed access (Buffer['allocUnsafe'](n)) and dynamic dispatch (global['Buffer'].allocUnsafe(n)) are not resolved.
  • Aliases (const alloc = Buffer.allocUnsafe; alloc(n);) are not tracked.
  • Buffer shims and polyfills that ship their own allocUnsafe are out of scope.
  • A loop write at a moving offset is treated as covering. A loop that writes only part of the buffer — an early break, a bound smaller than the allocation — is accepted on a coverage argument the rule did not check.

Landscape

PackageRuleBehavior
eslint-plugin-security-nodedetect-buffer-unsafe-allocationUnconditional; ships recommended: false
@microsoft/eslint-plugin-sdlno-unsafe-allocUnconditional
eslint-plugin-node-securityno-unsafe-buffer-allocallocUnsafeSlow too, the .fill() exemption, a covered-before-read check that clears the whole-buffer-copy idiom, a Buffer.alloc suggestion, and the CWE-789 wire-sized-allocation check

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.