Skip to content

fix(scan): stop js-uninitialized-buffer reporting buffers that are filled before use - #156

Merged
ralyodio merged 2 commits into
masterfrom
fix-uninitialized-buffer-fp
Aug 17, 2026
Merged

fix(scan): stop js-uninitialized-buffer reporting buffers that are filled before use#156
ralyodio merged 2 commits into
masterfrom
fix-uninitialized-buffer-fp

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Reported by a maintainer reviewing the scan workflow on mac-developer-bridge#7. Both of the rule's hits on that repository were correct code — a PTY ring buffer that writes every byte it later hands out:

const out = Buffer.allocUnsafe(length);
if (length === 0) return out;
buf.copy(out, 0, start, start + firstLen);
if (firstLen < length) buf.copy(out, firstLen, 0, length - firstLen);
return out;

The rule was a bare regex on Buffer.allocUnsafe(, so it fired on every call. Filling the buffer yourself is the entire reason that API exists over Buffer.alloc, which made this a report on correct code with no way to silence one occurrence without silencing all of them.

filledBeforeUseGuard exonerates an allocation written into before it escapes — copied into as a destination, filled or written through its own methods, set from a typed array, or assigned per index. It is bound to the name the allocation was assigned to, not to a write appearing nearby: a window guard that only asked "is there a .copy( around here" would exonerate the real defect whenever an unrelated buffer is filled below it. There is a test for exactly that.

Forward-only, sixteen lines. Ten was not enough for the shape that motivated it (allocation, early return, wrap-around arithmetic, then the copies).

Still reported: an allocation with no binding to follow. return Buffer.allocUnsafe(n) and socket.write(Buffer.allocUnsafe(n)) hand unzeroed heap straight out, which is the defect this rule is for.

The trade, stated plainly (and documented on the field): this cannot prove the write covers the whole buffer, so a partial fill is now exonerated and still leaks the remainder. Proving coverage needs range analysis this engine does not do. The alternative is the status quo, where the rule fires on every correct use, is read as noise and gets turned off — catching that partial write in exactly the same number of cases, namely none.

Verified against the reporting repository at 182fd94, end to end through the built CLI: 6 findings → 4. Both js-uninitialized-buffer hits gone, the other four still reported, and an unfilled allocation still flags. 302 tests pass in packages/scan, typecheck clean, pre-commit green.

Note this does not reach pinned consumers until the pack pin is bumped after a release.

…lled before use

Reported by a maintainer reviewing the scan workflow on mac-developer-bridge,
where both of the rule's hits were correct code — a PTY ring buffer that writes
every byte it later hands out:

    const out = Buffer.allocUnsafe(length);
    if (length === 0) return out;
    buf.copy(out, 0, start, start + firstLen);
    if (firstLen < length) buf.copy(out, firstLen, 0, length - firstLen);
    return out;

The rule was a bare regex on `Buffer.allocUnsafe(`, so it fired on every call.
Filling the buffer yourself is the entire reason that API exists over
`Buffer.alloc`, which made the rule a report on correct code with no way to
silence one occurrence without silencing all of them.

`filledBeforeUseGuard` exonerates an allocation that is written into before it
escapes: copied into as a destination, filled or written through its own
methods, `set` from a typed array, or assigned per index. It is bound to the
*name* the allocation was assigned to rather than to a write appearing nearby,
because a window guard that only asks "is there a `.copy(` around here" would
exonerate the real defect whenever an unrelated buffer is filled below it.
There is a test for exactly that.

Looks forward only, sixteen lines. Code that fills a buffer runs after the
allocation, and ten lines was not enough for the shape that motivated this —
allocation, early return, three or four lines of wrap-around arithmetic, then
the copies.

An allocation with no binding to follow is still reported. `return
Buffer.allocUnsafe(n)` and `socket.write(Buffer.allocUnsafe(n))` hand unzeroed
heap straight out, which is the defect this rule is for.

The trade is deliberate and documented on the field: this cannot prove the
write covers the whole buffer, so a partial fill is now exonerated and still
leaks the remainder. Proving coverage needs range analysis this engine does not
do. The alternative is the status quo, where the rule fires on every correct
use, is read as noise and gets turned off — catching that partial write in
exactly the same number of cases, namely none.

Verified against the reporting repository at 182fd94: its two
js-uninitialized-buffer findings are gone, its other four findings are still
reported, and an unfilled allocation still flags. 302 tests pass in
packages/scan; typecheck clean.
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

63 finding(s)

HIGH/CRITICAL: 4 | MEDIUM: 52 | LOW: 7

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
HIGH sh-eval-expansion .githooks/pre-commit:26
HIGH sh-remote-script-execution apps/web/public/install.sh:272
HIGH sh-remote-script-execution apps/web/public/install.sh:320
MEDIUM insecure-temp-file .githooks/commit-msg:16
MEDIUM insecure-temp-file .githooks/post-commit:20
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:70
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:79
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:92
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:31
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:33
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:34
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:35
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:36
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:43
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:56
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:63
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:82
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:84
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:85
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:93
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:98
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:105
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:112
MEDIUM js-shell-exec-interpolation apps/cli/src/index.ts:425
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:180
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:184
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:125
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:153
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:157
MEDIUM js-unescaped-html-sink apps/web/src/app/get-whitepaper/page.tsx:346
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:211
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:215
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:219
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:120
MEDIUM js-unescaped-html-sink apps/web/src/app/store/[slug]/page.tsx:86
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM manifest-install-lifecycle-script package.json:24
MEDIUM js-uninitialized-buffer packages/scan/src/node-rules.ts:456
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:154
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:160
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:208
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/apt.ts:313
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/aur.ts:256
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:264
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/chocolatey.ts:288
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:201
MEDIUM js-shell-exec-interpolation scripts/lib/package-managers/rpm.ts:261

…and 13 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread packages/scan/src/code-rules.ts Fixed
Comment thread packages/scan/src/code-rules.ts Fixed
Comment thread packages/scan/src/code-rules.ts Fixed
CodeQL failed the previous commit with three high-severity alerts, all of them
in the guard it added, and all three the same root cause: the name extracted
from the allocation was spliced into `new RegExp`.

  js/regex-injection          a pattern built from an extracted value
  js/incomplete-sanitization  the escaper handled `$` and not backslash
  js/polynomial-redos         `[A-Za-z_$][\w$]*` restarts at every position
                              inside a run of `$`, so the match is quadratic
                              in line length

The escaper only looked sufficient because the name came from a character class
that cannot contain a backslash — an argument that depends on a caller two
functions away and stops being true the first time someone reuses the helper.

Fixed patterns that *capture* a name, compared to the binding as a string.
Nothing is spliced, so nothing needs escaping and there is no constructed
pattern to be polynomial. The identifier shapes are pinned with a `(?<![\w$])`
lookbehind so they can only match where a name actually begins, which is what
makes the scan linear rather than quadratic.

A scanner that can be stalled by the file it is reading is a denial of service
in a CI gate, and this repository reports that class as
`redos-nested-quantifier` — shipping one in a false-positive fix would have
been its own kind of answer.

Two regression tests: a binding named `$buf$`, which is the shape that needed
escaping before and needs none now, and a 20,000-character run of `$` that has
to scan in well under a second.

Behaviour is unchanged. Re-verified against mac-developer-bridge at 182fd94:
still 6 findings → 4, both `js-uninitialized-buffer` hits gone, the other four
still reported, unfilled allocations still flagged. 304 tests pass in
packages/scan; typecheck clean.
@ralyodio
ralyodio merged commit 6d26c17 into master Aug 17, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants