Skip to content

Bump @fastify/busboy from 3.2.0 to 3.2.4 in packages/runtimes/serve-emu - #79

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

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

Conversation

@superbot-helper

Copy link
Copy Markdown

packages/runtimes/serve-emu depends on @fastify/busboy 3.2.0 and its own package-lock.json resolves that entry to 3.2.0, which carries three published advisories. This pull request moves the dependency spec and the one lock entry to 3.2.4. ws and every other lock entry are unchanged.

Two files change, because the dependency is pinned exactly: package.json "@fastify/busboy": "3.2.0" → "3.2.4" (same spec form, no range widening), and in package-lock.json the node_modules/@fastify/busboy entry plus the lock's own root "" dependency spec. A lock-only edit is not installable here — npm ci refuses it with EUSAGE: lock file's @fastify/busboy@3.2.4 does not satisfy @fastify/busboy@3.2.0 — so the manifest moves with it.

The advisories

advisory what fixed in
GHSA-xjh9-v7x6-24jw / CVE-2026-19484 (HIGH) DoS: a multipart boundary of exactly 252 bytes makes the streaming-search needle 256 bytes, the occurrence-table entry wraps to zero, and the Boyer-Moore loop never advances — the event loop spins. >= 3.1.0, < 3.2.1 3.2.1
GHSA-x8mw-p69m-v3mx / CVE-2026-19481 (HIGH) DoS: a part header named __proto__ or constructor resolves to an inherited non-array value 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) CRLF injection: a bare CR or LF inside a multipart Content-Disposition parameter is retained verbatim and delivered to the application. < 3.2.2 3.2.2

OSV returns three advisories for 3.2.0, one for 3.2.1, and none for 3.2.2, 3.2.3 and 3.2.4.

Why this parser is on an executed path here

packages/runtimes/serve-emu is the Node runtime behind the Mobile Dev plugin: src/cli.ts:164 picks an adb device (pickDevice, or --avd boots one), src/cli.ts:242 calls startServer(...), and src/node-server.ts:184 listens. The request chain to the parser is:

  • src/server.ts:1353 builds the API router from createApiRoutes(); src/server.ts:1448-1455 dispatches any request whose path is /api or /api/... (src/api/router.ts:68-69).
  • src/api/routes/applications.ts:12-19 registers POST /api/apps/install; :20-27 registers POST /api/files/import.
  • src/server.ts:1048-1053 (installEndpoint → uploadEndpoint, src/server.ts:982) calls stageUpload(req, { fieldName: "apk", ... }) at src/server.ts:1007 before it touches the device.
  • src/multipart-upload.ts:1 imports Busboy from @fastify/busboy; :444 constructs it with the request's content-type; :312 feeds it through parser.write(...).
  • src/multipart-upload.ts:19-24 and :68-86 are the endpoint's own parser feed: a 64 KiB + 8 KiB ring, and on finish() one write of everything except the last 8192 bytes, then one write of those 8192 bytes.

No authentication is involved on a loopback bind: src/server.ts:333-350 only enforces a token when one was configured, and --token defaults to off for 127.0.0.1 (src/cli.ts:181-186). The requests below sent no Authorization, cookie, token query or Origin header.

What was measured

The program was started from the bundle npm run build emits, produced with tooling/build-serve-emu.mjs's own esbuild invocation (--bundle --format=esm --platform=node --target=node22.18 --external:ws --external:@fastify/busboy), against a booted superbot-demo AVD on emulator-5556, as node src/cli.mjs -s emulator-5556 -p <port> — once with npm ci from the base lock (busboy 3.2.0) and once from this patch's lock (3.2.4). Each arm is one raw-socket POST /api/apps/install with the whole body in one client write, followed by a GET /api/devices on the same process. Times are time-to-first-byte.

arm 3.2.0 (base) 3.2.4 (this patch)
control: well-formed single part, filename="control.apk" 502 in 54.9 ms — adb-failed, INSTALL_PARSE_FAILED_NOT_APK; process alive 502 in 23.0 ms — same; process alive
part headers __proto__: polluted and constructor: also 400 in 3.5 ms — {"ok":false,"error":"this.header[h].push is not a function"}; process alive 502 in 52.9 ms — adb-failed (INSTALL_PARSE_FAILED_NOT_APK), i.e. the part parsed and the staged file reached adb install; process alive
filename="a\rb\nc.txt" 400 in 4.2 ms — {"ok":false,"error":"APK file must end with .apk"}: the parser delivered a\rb\nc.txt as the filename 400 in 2.3 ms — {"ok":false,"code":"unexpected-multipart-part","error":"multipart field apk must be a file"}: the part is skipped and never reaches the route
boundary token = 252 × A, false needle prefix away from the parser's flush point 502 in 37.1 ms — parses; process alive 502 in 22.1 ms — parses; process alive
boundary token = 252 × A, false needle prefix ending exactly at byte N−8192 (the parser's write boundary) no response: the client gave up at 12,002 ms, a follow-up GET /api/devices on the same process timed out at 4,002 ms, and the process's CPU time went from 0:00.19 to 0:15.67 while nothing was served 502 in 21.4 ms — parses, adb runs, process alive

The 502s are the emulator rejecting the staged file (INSTALL_PARSE_FAILED_NOT_APK), not a parser failure: the control and both boundary rows prove the parse completed and that the bytes were written to a temp file and handed to adb install. Both 400 rows are the route's own error envelope.

Parser level, against the packages npm ci installs from each lock, byte-identical payloads, one child process per run, 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) parses; file event with filename="proto.apk"
filename="a\rb\nc.txt" file event, filename bytes 97,13,98,10,99,46,116,120,116 no file event: the part is skipped
252-byte boundary, writes at N−8192 and 8192 no return; killed at 8,004 ms completes, 38 ms
the same bytes in one write completes, 38 ms completes, 37 ms

The last two rows are the point: the trigger is the fragmented delimiter write, not the boundary length on its own. In this endpoint the fragmentation is not the client's — the client sends all 28,824 bytes in one write() — it is the runtime's own ParserTrailerBuffer, which splits the body at N−8192 before handing it to busboy. The payload puts a false needle prefix (\r\n-- + 250 × A, then BB) so that the first write ends with a 254-byte prefix of the 256-byte needle and the second write starts 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 changes that table to Uint16Array (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 and not a memory error. The listener socket stays open; later requests on the same process time out. Nothing here was measured with more than one runtime process, and the plugin normally launches one CLI per workspace.
  • Every arm needs a booted device, because startServer opens a scrcpy session before it listens. The measurements use the repository's own --avd-less path with an externally booted AVD (-s emulator-5556). On a machine with no device the CLI exits at src/adb.ts:82 and no request can be made.
  • The 502 rows are pre-parse-clean and post-parse identical: the emulator rejects a non-APK upload, so a successful parse is visible as adb naming the staged temp file, not as a 200.
  • Only 3.2.0 and 3.2.4 were executed. 3.2.1, 3.2.2 and 3.2.3 were queried at OSV but no code was run against them.
  • The CRLF row is a difference in what the application receives (a File named a\rb\nc.txt that the route then rejects for not ending in .apk, versus a skipped part), not a claim that a CR/LF filename is rendered or stored anywhere.
  • 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 a defect, and nothing here depends on it.
  • GET /api/files/import (the other stageUpload caller) was not driven; it uses the same parser with fieldName: "file".
  • The repository's own test suite was not run: packages/runtimes/serve-emu has no test script and the workspace npm test covers other packages.

Verification

  • npm ci --no-audit --no-fund, run in a directory containing only this package's package.json and package-lock.json (a copy of packages/runtimes/serve-emu at the base commit, and the same directory with this patch applied): exit 0 on the base pair (installs @fastify/busboy 3.2.0) and exit 0 on this patch's pair (installs 3.2.4).
  • The lock diff is one package entry, node_modules/@fastify/busboy (version, resolved, integrity), plus the lock's root "" dependency spec; the ws entry is byte-identical.
  • @fastify/busboy@3.2.4 declares no dependencies and no engines, so the entry's absent dependencies block is correct for the new version.
  • No pull request in this repository, open or closed, has ever touched packages/runtimes/serve-emu/package.json or its package-lock.json (60 pull requests, all files checked). The repository has no .github/dependabot.yml.

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