From b7d034d75bce68531c6d072d7dcdad47e89b32a5 Mon Sep 17 00:00:00 2001 From: mushuiyu886 Date: Tue, 14 Jul 2026 00:29:41 +0800 Subject: [PATCH] fix(cli): reject noncanonical trajectory export requests (#105557) * fix(cli): reject noncanonical trajectory export requests * fix(cli): preserve encoded request bytes * style(cli): format trajectory alias tests * chore: keep trajectory note in PR context --------- Co-authored-by: Peter Steinberger Co-authored-by: Peter Steinberger --- src/commands/export-trajectory.test.ts | 21 +++++++++++++++++++++ src/commands/export-trajectory.ts | 13 ++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/commands/export-trajectory.test.ts b/src/commands/export-trajectory.test.ts index b7e845182594..585f20ff06d1 100644 --- a/src/commands/export-trajectory.test.ts +++ b/src/commands/export-trajectory.test.ts @@ -85,6 +85,27 @@ describe("exportTrajectoryCommand", () => { expect(runtime.exit).toHaveBeenCalledWith(1); }); + it.each([ + ["a discarded suffix", (encoded: string) => `${encoded}A`, "x"], + ["nonzero padding bits", (encoded: string) => `${encoded.slice(0, -1)}R`, "xy"], + ["surrounding whitespace", (encoded: string) => ` ${encoded} `, "xyz"], + ])( + "rejects a non-canonical base64url request with %s before looking up its session", + async (_case, makeNonCanonical, sessionKey) => { + const runtime = createRuntime(); + const canonical = Buffer.from(JSON.stringify({ sessionKey }), "utf8").toString("base64url"); + const requestJsonBase64 = makeNonCanonical(canonical); + + await exportTrajectoryCommand({ requestJsonBase64 }, runtime); + + expect(runtime.error).toHaveBeenCalledWith( + "Failed to decode trajectory export request: Encoded trajectory export request is invalid", + ); + expect(mocks.loadSessionEntry).not.toHaveBeenCalled(); + expect(runtime.exit).toHaveBeenCalledWith(1); + }, + ); + it("preserves direct options when an encoded request omits them", async () => { const runtime = createRuntime(); const requestJsonBase64 = Buffer.from( diff --git a/src/commands/export-trajectory.ts b/src/commands/export-trajectory.ts index b22e2a5b2cba..5a48f0a71e5c 100644 --- a/src/commands/export-trajectory.ts +++ b/src/commands/export-trajectory.ts @@ -43,13 +43,16 @@ function readOptionalString(value: unknown): string | undefined { } function decodeExportTrajectoryRequest(encoded: string): Partial { - const trimmed = encoded.trim(); - if (!ENCODED_EXPORT_REQUEST_RE.test(trimmed)) { + if (!ENCODED_EXPORT_REQUEST_RE.test(encoded)) { + throw new Error("Encoded trajectory export request is invalid"); + } + const bytes = Buffer.from(encoded, "base64url"); + if (bytes.toString("base64url") !== encoded) { throw new Error("Encoded trajectory export request is invalid"); } let decoded: unknown; try { - decoded = JSON.parse(Buffer.from(trimmed, "base64url").toString("utf8")) as unknown; + decoded = JSON.parse(bytes.toString("utf8")) as unknown; } catch { throw new Error("Encoded trajectory export request is invalid JSON"); } @@ -84,8 +87,8 @@ function decodeExportTrajectoryRequest(encoded: string): Partial