Skip to content

Commit bebd9d8

Browse files
committed
fix(sdk): bound the webhook key-template regex against ReDoS
The key placeholder regex used [^}]+, so a run of "{" with no closing brace backtracks quadratically: every start position rescans to the end. Exclude "{" from the character class so a non-matching position fails immediately (a valid {path} placeholder never contains "{"), making normalization linear.
1 parent 46a7f26 commit bebd9d8

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

packages/trigger-sdk/src/v3/webhooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export type {
306306
// Brace placeholders without a recognized namespace default to the event body. webhook./header./body.
307307
// pass through unchanged.
308308
export function normalizeKeyString(key: string): string {
309-
return key.replace(/\{([^}]+)\}/g, (_match, path: string) =>
309+
return key.replace(/\{([^{}]+)\}/g, (_match, path: string) =>
310310
path.startsWith("webhook.") || path.startsWith("header.") || path.startsWith("body.")
311311
? `{${path}}`
312312
: `{body.${path}}`
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from "vitest";
2+
import { normalizeKeyString } from "../src/v3/webhooks.js";
3+
4+
describe("normalizeKeyString", () => {
5+
it("passes through webhook./header./body. placeholders unchanged", () => {
6+
expect(normalizeKeyString("{body.event.id}")).toBe("{body.event.id}");
7+
expect(normalizeKeyString("{header.x-github-event}")).toBe("{header.x-github-event}");
8+
expect(normalizeKeyString("{webhook.deliveryId}")).toBe("{webhook.deliveryId}");
9+
});
10+
11+
it("defaults an unqualified placeholder to the event body", () => {
12+
expect(normalizeKeyString("{event.id}")).toBe("{body.event.id}");
13+
expect(normalizeKeyString("{conversationId}")).toBe("{body.conversationId}");
14+
});
15+
16+
it("normalizes every placeholder in a composite template", () => {
17+
expect(normalizeKeyString("{team}/{body.channel}/{event.ts}")).toBe(
18+
"{body.team}/{body.channel}/{body.event.ts}"
19+
);
20+
});
21+
22+
it("leaves literal text and unmatched braces alone", () => {
23+
expect(normalizeKeyString("no-placeholders")).toBe("no-placeholders");
24+
expect(normalizeKeyString("{{body.x}}")).toBe("{{body.x}}");
25+
});
26+
27+
it("handles a pathological brace run in linear time", { timeout: 1000 }, () => {
28+
const input = "{".repeat(500_000);
29+
expect(normalizeKeyString(input)).toBe(input);
30+
});
31+
});

0 commit comments

Comments
 (0)