authorgravatar for lucenz@proton.meSimone <lucenz@proton.me> 2026-10-14 04:34:55+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 23:34:55-03:00
log5e5abc92f472d764f232316d41f33e9159b81056
tree3c305ad14bd740ece86d52909d60263648bc4f68
parent5e5c52b7a70c16734e42faa072248a597e1333ee
signature Signed by PGP key B5690EEEBB952194

fix(server): avoid excessive backtracking in webhook templates (#19160)

Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com>

2 files changed, 23 insertions(+), 2 deletions(-)

apps/server/src/scheduledTasks/webhookTemplate.test.ts+19
......@@ -52,6 +52,25 @@ describe("renderWebhookPrompt", () => {
5252 assert.deepEqual(rendered.missing, ["body.nope", "headers.x-missing", "unknown"]);
5353 });
5454
55 it("trims placeholder whitespace before resolving and reporting missing fields", () => {
56 const rendered = renderWebhookPrompt(
57 "{{\tbody.action\n}}|{{ body.nope }}|{{\nbody.nope\t}}|{{ \t\n }}",
58 githubPullRequest,
59 );
60 assert.equal(rendered.prompt, "opened|||");
61 assert.deepEqual(rendered.missing, ["body.nope", ""]);
62 });
63
64 it("preserves unclosed whitespace-heavy placeholders and resolves later valid ones", () => {
65 const unfinished = `Notify {{${" ".repeat(1024)}body`;
66 const rendered = renderWebhookPrompt(
67 `${unfinished}\n{{body.pull_request.number}}`,
68 githubPullRequest,
69 );
70 assert.equal(rendered.prompt, `${unfinished}\n45`);
71 assert.deepEqual(rendered.missing, []);
72 });
73
5574 it("addresses form-encoded bodies and leaves other bodies to {{body}}", () => {
5675 const form = renderWebhookPrompt("{{body.text}} by {{body.user_name}}", {
5776 ...githubPullRequest,
apps/server/src/scheduledTasks/webhookTemplate.ts+4-2
......@@ -69,7 +69,8 @@ export function redactQuery(query: string): string {
6969 .join("&");
7070}
7171
72const PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g;
72// Trim after matching so unclosed placeholders cannot backtrack over whitespace partitions.
73const PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
7374
7475function parseBody(request: WebhookRequest): unknown {
7576 const contentType = (request.headers["content-type"] ?? "").toLowerCase();
......@@ -144,7 +145,8 @@ export function renderWebhookPrompt(
144145 }
145146 };
146147
147 const prompt = template.replace(PLACEHOLDER, (_match, expression: string) => {
148 const prompt = template.replace(PLACEHOLDER, (_match, rawExpression: string) => {
149 const expression = rawExpression.trim();
148150 const value = resolve(expression);
149151 if (value === undefined || value === null) {
150152 if (!missing.includes(expression)) missing.push(expression);