security(gateway): block prototype traversal in hook template paths

This commit is contained in:
Vincent Koc
2026-02-21 02:48:26 -05:00
parent 92ac6c95cc
commit 4151f95a19
2 changed files with 70 additions and 0 deletions

View File

@@ -372,4 +372,69 @@ describe("hooks mapping", () => {
});
expect(result?.ok).toBe(false);
});
describe("prototype pollution protection", () => {
it("blocks __proto__ traversal in webhook payload", async () => {
const mappings = resolveHookMappings({
mappings: [
createGmailAgentMapping({
id: "proto-test",
messageTemplate: "value: {{__proto__}}",
}),
],
});
const result = await applyHookMappings(mappings, {
payload: { __proto__: { polluted: true } } as Record<string, unknown>,
headers: {},
url: baseUrl,
path: "gmail",
});
expect(result?.ok).toBe(true);
if (result?.ok && result.action && result.action.kind === "agent") {
expect(result.action.message).toBe("value: ");
}
});
it("blocks constructor traversal in webhook payload", async () => {
const mappings = resolveHookMappings({
mappings: [
createGmailAgentMapping({
id: "constructor-test",
messageTemplate: "type: {{constructor.name}}",
}),
],
});
const result = await applyHookMappings(mappings, {
payload: { constructor: { name: "INJECTED" } } as Record<string, unknown>,
headers: {},
url: baseUrl,
path: "gmail",
});
expect(result?.ok).toBe(true);
if (result?.ok && result.action && result.action.kind === "agent") {
expect(result.action.message).toBe("type: ");
}
});
it("blocks prototype traversal in webhook payload", async () => {
const mappings = resolveHookMappings({
mappings: [
createGmailAgentMapping({
id: "prototype-test",
messageTemplate: "val: {{prototype}}",
}),
],
});
const result = await applyHookMappings(mappings, {
payload: { prototype: "leaked" } as Record<string, unknown>,
headers: {},
url: baseUrl,
path: "gmail",
});
expect(result?.ok).toBe(true);
if (result?.ok && result.action && result.action.kind === "agent") {
expect(result.action.message).toBe("val: ");
}
});
});
});

View File

@@ -80,6 +80,8 @@ const hookPresetMappings: Record<string, HookMappingConfig[]> = {
const transformCache = new Map<string, HookTransformFn>();
const BLOCKED_PATH_KEYS = new Set(["__proto__", "prototype", "constructor"]);
type HookTransformResult = Partial<{
kind: HookAction["kind"];
text: string;
@@ -465,6 +467,9 @@ function getByPath(input: Record<string, unknown>, pathExpr: string): unknown {
current = current[part] as unknown;
continue;
}
if (BLOCKED_PATH_KEYS.has(part)) {
return undefined;
}
if (typeof current !== "object") {
return undefined;
}