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 <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
mushuiyu886
2026-07-14 00:29:41 +08:00
committed by GitHub
parent 4a84117188
commit b7d034d75b
2 changed files with 29 additions and 5 deletions

View File

@@ -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(

View File

@@ -43,13 +43,16 @@ function readOptionalString(value: unknown): string | undefined {
}
function decodeExportTrajectoryRequest(encoded: string): Partial<ExportTrajectoryCommandOptions> {
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<ExportTrajector
function resolveExportTrajectoryOptions(
opts: ExportTrajectoryCommandOptions,
): ExportTrajectoryCommandOptions {
const encoded = opts.requestJsonBase64?.trim();
if (!encoded) {
const encoded = opts.requestJsonBase64;
if (encoded === undefined || encoded.length === 0) {
return opts;
}
return {