Skip to content

Bump @fastify/busboy from 3.2.0 to 3.2.4 in package-lock.json [SECURITY] - #1877

Open
superbot-helper wants to merge 1 commit into
nodeSolidServer:mainfrom
superbot-helper:security/bump-fastify-busboy-3.2.4
Open

superbot-helper wants to merge 1 commit into
nodeSolidServer:mainfrom
superbot-helper:security/bump-fastify-busboy-3.2.4

Conversation

@superbot-helper

Copy link
Copy Markdown

package.json declares "@fastify/busboy": "^3.2.0" (package.json:62) and package-lock.json resolves the single installed copy, node_modules/@fastify/busboy, to 3.2.0, which carries three published advisories. This pull request moves that one lock entry to 3.2.4, the current registry latest.

package.json is not touched: ^3.2.0 already admits 3.2.4, so the change is installable as a lock-only edit and the manifest stays byte-identical. There is exactly one lock entry that declares the package (the lock's own root "" entry, which mirrors package.json) and exactly one installed copy; there is no nested or second copy under any node_modules/.../node_modules path. npm ci exits 0 on both the base pair and this patch's pair.

The advisories

advisory what affected fixed in
GHSA-xjh9-v7x6-24jw / CVE-2026-19484 — HIGH, CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) DoS: a multipart boundary token of exactly 252 bytes makes the delimiter needle 256 bytes; 3.2.0 builds the streamsearch occurrence table as new Uint8Array(256).fill(256), which truncates every entry to 0, so a near-match split across two writes never advances pos += this._occ[ch] and the event loop spins >= 3.1.0, < 3.2.1 3.2.1
GHSA-x8mw-p69m-v3mx / CVE-2026-19481 — HIGH, CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) DoS: a part header named __proto__ or constructor resolves to an inherited non-array and the parser throws TypeError: this.header[h].push is not a function >= 1.0.0, < 3.2.1 3.2.1
GHSA-gxm5-99cw-xjw9 / CVE-2026-74866 — MODERATE, CVSS 3.1 AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N CRLF injection: a bare CR or LF inside a Content-Disposition filename/name parameter is retained verbatim and delivered to the application < 3.2.2 3.2.2

OSV returns these three for 3.2.0, one (GHSA-gxm5-99cw-xjw9) for 3.2.1, and none for 3.2.2, 3.2.3 or 3.2.4.

Why this parser is on an executed path here

  • lib/handlers/post.mjs:1 — import Busboy from '@fastify/busboy' (a static ESM import, so the package is loaded when the process starts, not per request).
  • lib/ldp-middleware.mjs:5 imports that handler and lib/ldp-middleware.mjs:25 registers it: router.post('/*', allow('Append'), post).
  • lib/create-app.mjs:137 mounts the router: app.use('/', LdpMiddleware(corsSettings, argv.prep)); lib/create-server.mjs:13 builds the app and :21 mounts it; bin/solid:2 → bin/lib/cli.mjs:20 → bin/lib/start.mjs:99 start it.
  • Inside the handler, lib/handlers/post.mjs:45 tests req.is('multipart/form-data') and :51 multi() constructs the parser at :54 (new Busboy({ headers: req.headers })) and feeds it at :76 (req.pipe(busboy)). Each file event is written into the container at :60 (ldp.put(putUrl, file, mimetype)).
  • There is no authentication on this path when webid is off: lib/handlers/allow.mjs:7-9 returns next() immediately when ldp.webid is false. The measurements below ran the server with SOLID_WEBID=false on 127.0.0.1, and every request carried no Authorization, cookie or token.

What was measured

The program was started as node bin/solid start --port <port> --root <dir> --no-live with SOLID_WEBID=false (plain HTTP; lib/create-server.mjs:34-36 only creates a TLS server when --ssl-key/--ssl-cert are given), once from a tree installed by npm ci against the base lock (@fastify/busboy 3.2.0) and once from a tree installed against this patch's lock (3.2.4). Each arm is one raw-socket POST / with Content-Type: multipart/form-data, followed by a GET / liveness probe on the same process. Times are from the client: TTFB where a response arrived, total where the client gave up.

A Module._load hook preloaded into the server process logged the package load at startup — [busboy-load] request=…/@fastify/busboy/lib/main.js … version=3.2.0 — so the parser is loaded by the server itself, not by the harness. A positive control (node --require hook.cjs control-esm.mjs) logs the same line; a negative control (requiring an unrelated module) logs none.

arm (all POST /, one part) 3.2.0 (base) 3.2.4 (this patch)
control: filename="control.txt", 152-byte body 200 in 5.01 ms, control.txt (15 bytes) stored; process alive 200 in 6.68 ms, control.txt (15 bytes) stored; process alive
part headers __proto__: polluted and constructor: also 500 in 2.50 ms, body Error receiving the file; the server log carries solid:post Error receiving the file: this.header[h].push is not a function; no file stored 200 in 2.81 ms, proto.txt (15 bytes) stored — the part parsed and reached ldp.put
filename="a\rb\nc.txt" (raw CR then raw LF inside the quotes) 200 in 3.36 ms; the parser delivered the name verbatim and the file was stored on disk as a\rb\nc.txt with the CR and LF inside the name (15 bytes) 200 in 2.67 ms; no file event, nothing stored — the part is skipped
boundary token 252 × A, one client write, 24,866-byte body 200 in 2.21 ms; frag.txt (24,256 bytes) stored 200 in 2.86 ms; frag.txt (24,256 bytes) stored
the same bytes with the first 20,604 bytes written, a 150 ms pause, then the remaining 4,262 bytes (the cut lands inside the false needle prefix) no response: the client gave up at 12,001.54 ms; a GET / on the same process then timed out at 4,001 ms; the process's CPU time went 0:00.42 → 0:12.21 while serving nothing; frag.txt was left at 20,000 of its 24,256 bytes 200 in 153.28 ms (the 150 ms is the client's own pause); frag.txt (24,256 bytes) stored; the follow-up GET / returned 200

Parser level, against the packages each npm ci installed, byte-identical payloads, one child process per row, killed at 8 s:

probe 3.2.0 3.2.4
__proto__ / constructor part headers throws TypeError: this.header[h].push is not a function at deps/dicer/lib/HeaderParser.js:95 (uncaught, child exits 1) completes in 2.05 ms; file event proto.txt
filename="a\rb\nc.txt" completes; file event with filename bytes 97,13,98,10,99,46,116,120,116 completes; no file event (part skipped)
252-byte boundary, writes at 20,604 and 4,262 no return; killed at 8,002 ms completes in 153.26 ms
the same bytes in one write completes in 1.67 ms completes in 1.52 ms

The last two rows are the point: a 252-byte boundary alone is not a denial of service. The trigger is the near-match split across two writes. In this endpoint the split is the client's — the request body arrives over the socket in whatever chunks the client writes — so a client that writes the body in two pieces at a chosen offset gets the spin deterministically; the same bytes in one write complete on both versions in under 2 ms. The payload places \r\n-- + 250 × A + BB in the body so that the first write ends after 254 bytes of the 256-byte needle and the second begins with B, a byte the 3.2.0 occurrence table has no entry for; deps/streamsearch/sbmh.js:129 (pos += this._occ[ch]) then advances by zero. 3.2.4 builds that table as new Uint16Array(256) (sbmh.js:56), so 256 no longer wraps to 0.

Limits

  • The denial of service is a hung event loop in the process that parses, not a crash. The listener socket stays open; later requests on the same process time out. The measurement is one Node process serving one HTTP listener. Nothing here was measured about a supervisor, a restart, or a multi-process deployment.
  • The 252-byte arm needs the fragmented write. The same 24,866-byte body in one write returns 200 in 2.21 ms on 3.2.0, so a 252-byte boundary token is not claimed as a denial of service by itself. The fragmentation used here is two client writes with a 150 ms pause; that is a real write boundary, but it is the client's, not one the server imposes.
  • A boundary token of 253 bytes or more throws The needle cannot have a length bigger than 256 on every version including 3.2.4; that is a constructor guard, not one of the advisories, and nothing here depends on it.
  • Only 3.2.0 and 3.2.4 were executed. 3.2.1, 3.2.2 and 3.2.3 were queried at OSV; no code ran against them.
  • The CRLF row is a difference in what the application receives and stores — a file whose name contains a raw CR and LF, versus no part at all — not a claim about a header or a log line rendered elsewhere. Whether a name with a CR or LF is desirable is the application's decision; the parser no longer passes one through.
  • The __proto__ row's 500 is the handler's own envelope (lib/handlers/post.mjs:65-67); the parser's TypeError message appears in the server log and, uncaught, in the parser-level probe. The status is the application's, the exception is the parser's.
  • The route was driven as POST / against the storage root. Other container paths are the same handler (router.post('/*', …), lib/ldp-middleware.mjs:25); no other path was driven.
  • The server ran with SOLID_WEBID=false. With WebID on, allow.mjs performs an ACL check before the handler; that path was not exercised, and the parser is reached either way once the request is authorized.
  • The repository's own test suite was not run: npm test runs eslint, a Turtle validator and the full c8 mocha suite against live servers, and it takes far longer than a dependency bump warrants. npm ci and the running-program arms are the gates that were run.
  • No pull request in this repository, open or closed, moves @fastify/busboy past 3.2.0 (916 PRs screened, every changed-file list read, every lock-touching diff classified, the six too-large diffs read from their head locks). The only PR that names the package is update busboy to @fastify/busboy #1647, merged 2021-12-27, which migrated busboy@0.3.1 to @fastify/busboy@1.0.0.

Verification

  • npm ci --no-audit --no-fund on a copy of the base tree: exit 0, 1,315 packages, installs @fastify/busboy 3.2.0. The same command on the same tree with this patch's package-lock.json: exit 0, 1,315 packages, installs 3.2.4. package.json is byte-identical in both trees (cmp exit 0).
  • The lock diff is one entry, node_modules/@fastify/busboy (version, resolved, integrity), +3/−3 lines. The lock's root "" entry and every other entry are byte-identical; the entry count stays 1,326.
  • @fastify/busboy@3.2.4 declares no dependencies and no engines, so the entry's absent dependencies block is correct for the new version.
  • patch.diff applied to the base lock reproduces the shipped lock byte for byte (sha256 d825386a…), and that file is the blob in the commit this branch carries.
  • The repository has no .github/dependabot.yml. It has a renovate.json (extends: config:base), but the newest Renovate pull request is Update dependency snyk to v1.427.1 #1516 from 2020-11-12.

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.

1 participant