fix(canvas): cap diff canvas size in schema (codex pass 13 P2)
Agent-emitted diff canvases had no line/hunk/file caps in the Zod schema. With the existing 256 KB byte budget per canvas an attacker or buggy agent could pack tens of thousands of one-character add/del lines, causing CanvasDiff to render hundreds of thousands of DOM rows on every 5s poll and freeze the session detail page. Adds three caps in canvas-schema.ts mirroring the synthesized git-diff path's existing constants: DIFF_MAX_LINES_PER_HUNK = 1000 DIFF_MAX_HUNKS_PER_FILE = 50 DIFF_MAX_FILES_PER_CANVAS = 200 Two new tests prove over-cap diffs are rejected. Refs: docs/canvases-codex-review-2026-05-06.html (the P2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b8c245fae
commit
89cf367e5f
|
|
@ -134,6 +134,44 @@ describe("CanvasArtifactSchema", () => {
|
|||
const result = CanvasArtifactSchema.safeParse({ ...validMarkdown, id: "BAD ID" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects diff canvases with too many hunk lines", () => {
|
||||
// 1001 lines exceeds DIFF_MAX_LINES_PER_HUNK (1000). This is the cap that
|
||||
// prevents agent-emitted diffs from rendering hundreds of thousands of DOM
|
||||
// rows on every 5s poll.
|
||||
const tooManyLines = Array.from({ length: 1001 }, (_, i) => ({
|
||||
kind: "context" as const,
|
||||
text: `line ${i}`,
|
||||
}));
|
||||
const result = CanvasArtifactSchema.safeParse({
|
||||
version: 1,
|
||||
id: "huge",
|
||||
type: "diff",
|
||||
title: "Too many lines",
|
||||
createdAt: "2026-05-06T00:00:00Z",
|
||||
updatedAt: "2026-05-06T00:00:00Z",
|
||||
payload: { files: [{ path: "a.ts", status: "modified", hunks: [{ header: "@@", lines: tooManyLines }] }] },
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects diff canvases with too many files", () => {
|
||||
const tooManyFiles = Array.from({ length: 201 }, (_, i) => ({
|
||||
path: `f${i}.ts`,
|
||||
status: "modified" as const,
|
||||
hunks: [],
|
||||
}));
|
||||
const result = CanvasArtifactSchema.safeParse({
|
||||
version: 1,
|
||||
id: "wide",
|
||||
type: "diff",
|
||||
title: "Too many files",
|
||||
createdAt: "2026-05-06T00:00:00Z",
|
||||
updatedAt: "2026-05-06T00:00:00Z",
|
||||
payload: { files: tooManyFiles },
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseUnifiedDiff", () => {
|
||||
|
|
|
|||
|
|
@ -24,10 +24,19 @@ const diffLine = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
// Bounds matching the synthesized git-diff path's caps in canvas-log.ts.
|
||||
// Without these, an agent-emitted diff canvas could pack tens of thousands of
|
||||
// hunk lines into a 256 KB payload and stall the session detail page on every
|
||||
// 5s poll. Numbers chosen to be generous for legitimate review/refactor diffs
|
||||
// while preventing DOM blowup. See canvas-log.ts: DIFF_MAX_FILES, DIFF_MAX_LINES.
|
||||
const DIFF_MAX_LINES_PER_HUNK = 1000;
|
||||
const DIFF_MAX_HUNKS_PER_FILE = 50;
|
||||
const DIFF_MAX_FILES_PER_CANVAS = 200;
|
||||
|
||||
const diffHunk = z
|
||||
.object({
|
||||
header: z.string(),
|
||||
lines: z.array(diffLine),
|
||||
lines: z.array(diffLine).max(DIFF_MAX_LINES_PER_HUNK),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
|
@ -36,13 +45,13 @@ const diffFile = z
|
|||
path: z.string().min(1),
|
||||
oldPath: z.string().optional(),
|
||||
status: z.enum(["added", "modified", "deleted", "renamed"]),
|
||||
hunks: z.array(diffHunk),
|
||||
hunks: z.array(diffHunk).max(DIFF_MAX_HUNKS_PER_FILE),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const diffPayload = z
|
||||
.object({
|
||||
files: z.array(diffFile),
|
||||
files: z.array(diffFile).max(DIFF_MAX_FILES_PER_CANVAS),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue