From 4a45c14742e6868f687cead2ddf736c3f7c49acd Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 9 Oct 2026 04:40:47 +0800 Subject: [PATCH] fix(firmware): reject malformed UTF-8 producer reports --- docs/firmware-analysis.md | 5 ++- src/firmware/FirmwareFiles.ts | 6 ++- .../firmware/firmwareReportFiles.test.ts | 43 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/boundary/firmware/firmwareReportFiles.test.ts diff --git a/docs/firmware-analysis.md b/docs/firmware-analysis.md index 1796ef43a..12886bd0d 100644 --- a/docs/firmware-analysis.md +++ b/docs/firmware-analysis.md @@ -129,8 +129,9 @@ tree semantics, vulnerability scanning, emulation or hardware runtime capture. Unblob one worker. No automatic parallel extraction engines. - **120-second** deadline; inherited **1 GiB per-process address-space** limit, 120 CPU seconds and zero core-dump allowance through `prlimit`. -- JSON reports: at most **8 MiB**; oversized reports fail without a truncated - success. Diagnostic output is bounded with exact captured byte counts. +- JSON reports: at most **8 MiB** and valid UTF-8; malformed encoding and + oversized reports fail without repairing or truncating producer data. + Diagnostic output is bounded with exact captured byte counts. - Default staging budget: **256 MiB** and **10,000 entries**, configurable using `max_output_bytes` / `max_output_files` (CLI `--max-output-bytes` / `--max-output-files`). Depth defaults to 3, allowed range 1–10. diff --git a/src/firmware/FirmwareFiles.ts b/src/firmware/FirmwareFiles.ts index ad63b49de..a118984b8 100644 --- a/src/firmware/FirmwareFiles.ts +++ b/src/firmware/FirmwareFiles.ts @@ -153,7 +153,11 @@ export const readFirmwareReport = async ( `Report exceeded ${FIRMWARE_LIMITS.reportBytes} bytes`, ); try { - const value: unknown = JSON.parse(bytes.toString("utf8")); + const value: unknown = JSON.parse( + new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode( + bytes, + ), + ); return value; } catch (cause: unknown) { throw new AnalysisOutputError( diff --git a/tests/boundary/firmware/firmwareReportFiles.test.ts b/tests/boundary/firmware/firmwareReportFiles.test.ts new file mode 100644 index 000000000..f3a181624 --- /dev/null +++ b/tests/boundary/firmware/firmwareReportFiles.test.ts @@ -0,0 +1,43 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; + +import { readFirmwareReport } from "../../../src/firmware/FirmwareFiles.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it.each([ + ["invalid leading byte", [0xff]], + ["incomplete sequence", [0xe2, 0x82]], + ["overlong sequence", [0xc0, 0xaf]], + ["encoded surrogate", [0xed, 0xa0, 0x80]], +])( + "rejects complete JSON containing %s without repairing producer bytes", + async (_name, bytes) => { + const root = await createTestTempDirectory("rea-firmware-report-"); + const path = join(root, "report.json"); + await writeFile( + path, + Buffer.concat([ + Buffer.from('{"description":"'), + Buffer.from(bytes), + Buffer.from('"}'), + ]), + ); + await expect( + readFirmwareReport(path, "inspect_firmware_regions"), + ).rejects.toMatchObject({ + _tag: "AnalysisOutputError", + reason: "Tool report is not valid complete JSON", + }); + }, +); + +it("preserves valid Unicode and a literal replacement character in producer JSON", async () => { + const root = await createTestTempDirectory("rea-firmware-report-"); + const path = join(root, "report.json"); + const report = { description: "固件 🧩 \ufffd", path: "/输入/firmware.bin" }; + await writeFile(path, JSON.stringify(report)); + expect(await readFirmwareReport(path, "inspect_firmware_regions")).toEqual( + report, + ); +});