




























@@ -181,4 +181,140 @@ describe("cron tool flat-params", () => {
181181staggerMs: 30_000,
182182});
183183});
184+185+it("trims trailing whitespace from recognized job object keys (#95407)", async () => {
186+const tool = createCronTool(undefined, { callGatewayTool: callGatewayToolMock });
187+188+await tool.execute("call-trailing-space", {
189+action: "add",
190+job: {
191+name: "Holiday Check-in",
192+description: "Casual check-in",
193+"schedule ": { kind: "cron", expr: "30 10,20 * * *", tz: "Europe/Madrid" },
194+"sessionTarget ": "isolated",
195+"payload ": { kind: "agentTurn", message: "How's it going?" },
196+"enabled ": true,
197+},
198+});
199+200+const [method, _gatewayOpts, params] = firstGatewayToolCall<{
201+name?: string;
202+schedule?: unknown;
203+sessionTarget?: string;
204+payload?: unknown;
205+enabled?: boolean;
206+}>();
207+expect(method).toBe("cron.add");
208+expect(params.name).toBe("Holiday Check-in");
209+expect(params.schedule).toBeDefined();
210+expect(params.sessionTarget).toBe("isolated");
211+expect(params.payload).toBeDefined();
212+expect(params.enabled).toBe(true);
213+expect(params).not.toHaveProperty("schedule ");
214+expect(params).not.toHaveProperty("sessionTarget ");
215+expect(params).not.toHaveProperty("payload ");
216+expect(params).not.toHaveProperty("enabled ");
217+});
218+219+it("trims trailing whitespace from recognized patch object keys (#95407)", async () => {
220+const tool = createCronTool(undefined, { callGatewayTool: callGatewayToolMock });
221+222+await tool.execute("call-patch-trailing-space", {
223+action: "update",
224+jobId: "job-123",
225+patch: {
226+"schedule ": { kind: "cron", expr: "0 9 * * 1-5", tz: "America/New_York" },
227+"enabled ": false,
228+},
229+});
230+231+const [method, _gatewayOpts, params] = firstGatewayToolCall<{
232+id?: string;
233+patch?: { schedule?: unknown; enabled?: boolean };
234+}>();
235+expect(method).toBe("cron.update");
236+expect(params.id).toBe("job-123");
237+expect(params.patch?.schedule).toBeDefined();
238+expect((params.patch?.schedule as Record<string, unknown>)?.kind).toBe("cron");
239+expect(params.patch?.enabled).toBe(false);
240+expect(params.patch).not.toHaveProperty("schedule ");
241+expect(params.patch).not.toHaveProperty("enabled ");
242+});
243+244+it("does not trim unrecognized keys to prevent prototype pollution (#95407)", async () => {
245+const tool = createCronTool(undefined, { callGatewayTool: callGatewayToolMock });
246+247+await tool.execute("call-unsafe-keys", {
248+action: "add",
249+job: {
250+name: "Safe trim",
251+schedule: { kind: "cron", expr: "0 12 * * *", tz: "UTC" },
252+payload: { kind: "agentTurn", message: "work" },
253+// Non-recognized keys with trailing spaces should NOT be trimmed
254+// (prevents "__proto__ " → "__proto__" style attacks)
255+"__proto__ ": { malicious: true },
256+"constructor ": "should not be trimmed",
257+},
258+});
259+260+const [method, _gatewayOpts, params] = firstGatewayToolCall<Record<string, unknown>>();
261+expect(method).toBe("cron.add");
262+// Non-recognized padded keys should remain as-is
263+expect(params).toHaveProperty("__proto__ ");
264+expect(params).toHaveProperty("constructor ");
265+});
266+267+it("preserves padded duplicate when canonical key already exists (#95407)", async () => {
268+// When both canonical and padded forms exist, the padded key is preserved
269+// so strict gateway validation rejects the ambiguous input rather than
270+// silently picking one value.
271+const tool = createCronTool(undefined, { callGatewayTool: callGatewayToolMock });
272+273+await tool.execute("call-duplicate-keys", {
274+action: "add",
275+job: {
276+name: "Duplicate test",
277+schedule: { kind: "cron", expr: "0 9 * * 1-5", tz: "UTC" },
278+// Both "schedule" and "schedule " exist — padded preserved for rejection
279+"schedule ": { kind: "every", everyMs: 60000 },
280+payload: { kind: "agentTurn", message: "work" },
281+"enabled ": true,
282+enabled: false,
283+},
284+});
285+286+const [method, _gatewayOpts, params] = firstGatewayToolCall<Record<string, unknown>>();
287+expect(method).toBe("cron.add");
288+// Canonical key is untouched
289+expect((params.schedule as Record<string, unknown>)?.kind).toBe("cron");
290+expect(params.enabled).toBe(false);
291+// Padded keys are preserved so gateway schema validation sees the conflict
292+// and rejects with "unexpected property 'schedule '" instead of silently
293+// accepting one of the two conflicting values.
294+expect(params).toHaveProperty("schedule ");
295+expect(params).toHaveProperty("enabled ");
296+});
297+298+it("preserves normal keys without any whitespace", async () => {
299+const tool = createCronTool(undefined, { callGatewayTool: callGatewayToolMock });
300+301+await tool.execute("call-clean-keys", {
302+action: "add",
303+job: {
304+name: "Clean keys",
305+schedule: { kind: "cron", expr: "0 12 * * *", tz: "UTC" },
306+payload: { kind: "agentTurn", message: "test" },
307+enabled: true,
308+description: "All keys should be preserved as-is",
309+},
310+});
311+312+const [method, _gatewayOpts, params] = firstGatewayToolCall<Record<string, unknown>>();
313+expect(method).toBe("cron.add");
314+expect(params.name).toBe("Clean keys");
315+expect(params.schedule).toBeDefined();
316+expect(params.payload).toBeDefined();
317+expect(params.enabled).toBe(true);
318+expect(params.description).toBe("All keys should be preserved as-is");
319+});
184320});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。