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", () => {...@@ -52,6 +52,25 @@ describe("renderWebhookPrompt", () => {
52 assert.deepEqual(rendered.missing, ["body.nope", "headers.x-missing", "unknown"]);52 assert.deepEqual(rendered.missing, ["body.nope", "headers.x-missing", "unknown"]);
53 });53 });
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
55 it("addresses form-encoded bodies and leaves other bodies to {{body}}", () => {74 it("addresses form-encoded bodies and leaves other bodies to {{body}}", () => {
56 const form = renderWebhookPrompt("{{body.text}} by {{body.user_name}}", {75 const form = renderWebhookPrompt("{{body.text}} by {{body.user_name}}", {
57 ...githubPullRequest,76 ...githubPullRequest,
apps/server/src/scheduledTasks/webhookTemplate.ts+4-2
...@@ -69,7 +69,8 @@ export function redactQuery(query: string): string {...@@ -69,7 +69,8 @@ export function redactQuery(query: string): string {
69 .join("&");69 .join("&");
70}70}
7171
72const PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g;72// Trim after matching so unclosed placeholders cannot backtrack over whitespace partitions.
73const PLACEHOLDER = /\{\{([^{}]*)\}\}/g;
7374
74function parseBody(request: WebhookRequest): unknown {75function parseBody(request: WebhookRequest): unknown {
75 const contentType = (request.headers["content-type"] ?? "").toLowerCase();76 const contentType = (request.headers["content-type"] ?? "").toLowerCase();
...@@ -144,7 +145,8 @@ export function renderWebhookPrompt(...@@ -144,7 +145,8 @@ export function renderWebhookPrompt(
144 }145 }
145 };146 };
146147
147 const prompt = template.replace(PLACEHOLDER, (_match, expression: string) => {148 const prompt = template.replace(PLACEHOLDER, (_match, rawExpression: string) => {
149 const expression = rawExpression.trim();
148 const value = resolve(expression);150 const value = resolve(expression);
149 if (value === undefined || value === null) {151 if (value === undefined || value === null) {
150 if (!missing.includes(expression)) missing.push(expression);152 if (!missing.includes(expression)) missing.push(expression);