Skip to content

fix: add authentication to incident webhook endpoint#1600

Open
Akshita-2307 wants to merge 1 commit into
nisshchayarathi:mainfrom
Akshita-2307:fix/incident-webhook-auth-1539
Open

fix: add authentication to incident webhook endpoint#1600
Akshita-2307 wants to merge 1 commit into
nisshchayarathi:mainfrom
Akshita-2307:fix/incident-webhook-auth-1539

Conversation

@Akshita-2307
Copy link
Copy Markdown
Contributor

@Akshita-2307 Akshita-2307 commented Jun 1, 2026

The incident webhook accepted unauthenticated POST requests and allowed installationId, owner, and repo via query params with unsafe defaults. Added x-webhook-secret header validation via INCIDENT_WEBHOOK_SECRET env var, and validates installationId as a number.

Closes #1539

Summary by CodeRabbit

  • New Features
    • Added optional authentication for webhook requests via secret validation
    • Enhanced request parameter validation with improved error handling

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

Someone is attempting to deploy a commit to the Nisshchaya's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The incident webhook POST handler is hardened with two security checks: conditional HMAC authentication using an environment-configured secret header, and explicit parsing and validation of the required installationId query parameter to prevent processing requests with invalid or missing context identifiers.

Changes

Webhook security hardening

Layer / File(s) Summary
Webhook authentication and parameter validation
app/api/integrations/incidents/webhook/route.ts
Adds conditional HMAC authentication comparing INCIDENT_WEBHOOK_SECRET environment variable against x-webhook-secret request header, rejecting with 401 on mismatch. Parses and validates installationId from query parameters, rejecting with 400 on NaN values.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

security, bug, level:critical

A webhook that was careless and free,
Now checks its secrets and IDs with glee!
No forged incidents shall pass through,
With validation and auth, now strong and true! 🔐🐰

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements partial mitigation: it adds x-webhook-secret header validation against INCIDENT_WEBHOOK_SECRET and validates installationId as numeric, but does not implement HMAC sha256 signature verification or validate owner/repo parameters as required. Implement HMAC sha256 signature verification over raw request body using x-incident-signature-256 header format (sha256=...), validate owner and repo parameters are non-empty, and add unit tests for signature validation and parameter parsing.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding authentication to the incident webhook endpoint.
Out of Scope Changes check ✅ Passed All changes are directly related to addressing the webhook authentication vulnerability; no out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the GSSoC'26 Part of GirlScript Summer of Code 2026 label Jun 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

🎉 Thanks for your contribution, @Akshita-2307!

Your PR has passed our automated GSSoC quality checks. Here's a quick summary:

Check Status
PR description ✅ Provided
PR title ✅ Meaningful
Linked issue ✅ Found
Change size ✅ Looks good (13 lines across 1 file(s))

A maintainer will review your PR soon. Please be patient and available for feedback. 💪

GSSoC'26 automation · Maintainer: @nisshchayarathi

1 similar comment
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 1, 2026

🎉 Thanks for your contribution, @Akshita-2307!

Your PR has passed our automated GSSoC quality checks. Here's a quick summary:

Check Status
PR description ✅ Provided
PR title ✅ Meaningful
Linked issue ✅ Found
Change size ✅ Looks good (13 lines across 1 file(s))

A maintainer will review your PR soon. Please be patient and available for feedback. 💪

GSSoC'26 automation · Maintainer: @nisshchayarathi

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/integrations/incidents/webhook/route.ts`:
- Around line 33-38: The code currently uses unsafe defaults for installationId,
owner, and repo; remove the fallback defaults and instead validate presence and
correctness: for installationId call url.searchParams.get("installationId") and
if null or parseInt(...,10) yields NaN return NextResponse.json({ error:
"Missing or invalid installationId" }, { status: 400 }); similarly require
url.searchParams.get("owner") and url.searchParams.get("repo") to be
non-null/non-empty and if missing return 400 (e.g. NextResponse.json({ error:
"Missing owner" }, { status: 400 }) and "Missing repo" respectively); update the
code paths referencing installationId, owner, and repo to use these validated
values (no default "1", "owner", or "repo").
- Around line 10-16: The current secret header check must be replaced with
HMAC-SHA256 verification: require INCIDENT_WEBHOOK_SECRET (fail closed if
unset), read the raw request body (use the Request object raw bytes), compute
HMAC-SHA256 with the webhookSecret and compare to the x-incident-signature-256
header using crypto.timingSafeEqual for constant-time comparison, reject if
missing/invalid; also add replay protection by validating an
x-incident-timestamp header within an allowed skew window before accepting.
Update the code paths referencing webhookSecret and the header check in route.ts
to perform these steps and return 401 on any failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 16fe0fb5-1bba-4396-a406-95d5f1d4026d

📥 Commits

Reviewing files that changed from the base of the PR and between 63ecb90 and 8fec13f.

📒 Files selected for processing (1)
  • app/api/integrations/incidents/webhook/route.ts

Comment on lines +10 to +16
const webhookSecret = process.env.INCIDENT_WEBHOOK_SECRET;
if (webhookSecret) {
const headerSecret = req.headers.get("x-webhook-secret");
if (!headerSecret || headerSecret !== webhookSecret) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Security implementation does not match issue #1539 requirements.

Several critical gaps exist between this implementation and the security requirements:

  1. No HMAC signature verification: Issue #1539 requires verifying an HMAC-SHA256 signature (x-incident-signature-256) over the raw request body. The current implementation compares a plain secret transmitted in headers, which:

    • Exposes the secret in transit (even over HTTPS, logs/proxies may capture headers)
    • Provides no request body integrity verification
    • Offers no replay protection
  2. Conditional authentication bypass: When INCIDENT_WEBHOOK_SECRET is unset, the endpoint remains fully unauthenticated. This should fail closed.

  3. Timing attack vulnerability: Using !== for secret comparison leaks timing information. Use crypto.timingSafeEqual().

🔒 Proposed fix implementing HMAC verification
+import crypto from "crypto";
 import { NextRequest, NextResponse } from "next/server";
 // ... other imports

 export async function POST(req: NextRequest) {
   try {
     const webhookSecret = process.env.INCIDENT_WEBHOOK_SECRET;
-    if (webhookSecret) {
-      const headerSecret = req.headers.get("x-webhook-secret");
-      if (!headerSecret || headerSecret !== webhookSecret) {
-        return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
-      }
+    if (!webhookSecret) {
+      console.error("[WebhookRoute] INCIDENT_WEBHOOK_SECRET not configured");
+      return NextResponse.json({ error: "Webhook not configured" }, { status: 500 });
     }
+
+    const signature = req.headers.get("x-incident-signature-256");
+    if (!signature) {
+      return NextResponse.json({ error: "Missing signature" }, { status: 401 });
+    }
+
+    const rawBody = await req.text();
+    const expectedSignature = "sha256=" + crypto
+      .createHmac("sha256", webhookSecret)
+      .update(rawBody)
+      .digest("hex");
+
+    const sigBuffer = Buffer.from(signature);
+    const expectedBuffer = Buffer.from(expectedSignature);
+    if (sigBuffer.length !== expectedBuffer.length || 
+        !crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
+      return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
+    }
+
+    const payload = JSON.parse(rawBody);
-
-    const payload = await req.json();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const webhookSecret = process.env.INCIDENT_WEBHOOK_SECRET;
if (webhookSecret) {
const headerSecret = req.headers.get("x-webhook-secret");
if (!headerSecret || headerSecret !== webhookSecret) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
}
const webhookSecret = process.env.INCIDENT_WEBHOOK_SECRET;
if (!webhookSecret) {
console.error("[WebhookRoute] INCIDENT_WEBHOOK_SECRET not configured");
return NextResponse.json({ error: "Webhook not configured" }, { status: 500 });
}
const signature = req.headers.get("x-incident-signature-256");
if (!signature) {
return NextResponse.json({ error: "Missing signature" }, { status: 401 });
}
const rawBody = await req.text();
const expectedSignature = "sha256=" + crypto
.createHmac("sha256", webhookSecret)
.update(rawBody)
.digest("hex");
const sigBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expectedSignature);
if (sigBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const payload = JSON.parse(rawBody);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/integrations/incidents/webhook/route.ts` around lines 10 - 16, The
current secret header check must be replaced with HMAC-SHA256 verification:
require INCIDENT_WEBHOOK_SECRET (fail closed if unset), read the raw request
body (use the Request object raw bytes), compute HMAC-SHA256 with the
webhookSecret and compare to the x-incident-signature-256 header using
crypto.timingSafeEqual for constant-time comparison, reject if missing/invalid;
also add replay protection by validating an x-incident-timestamp header within
an allowed skew window before accepting. Update the code paths referencing
webhookSecret and the header check in route.ts to perform these steps and return
401 on any failure.

Comment on lines 33 to 38
const installationId = parseInt(url.searchParams.get("installationId") || "1", 10);
if (isNaN(installationId)) {
return NextResponse.json({ error: "Invalid installationId" }, { status: 400 });
}
const owner = url.searchParams.get("owner") || "owner";
const repo = url.searchParams.get("repo") || "repo";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Unsafe defaults for installationId, owner, and repo violate issue requirements.

Issue #1539 explicitly states: "Reject missing or invalid installationId, owner, and repo (do not rely on unsafe defaults)."

Current behavior:

  • installationId defaults to "1" when missing—the isNaN check only catches malformed strings like "abc", not absent parameters
  • owner defaults to "owner"
  • repo defaults to "repo"

These defaults allow attackers to omit parameters and trigger rollback automation against arbitrary repository contexts.

🛡️ Proposed fix to reject missing parameters
     const url = new URL(req.url);
-    const installationId = parseInt(url.searchParams.get("installationId") || "1", 10);
-    if (isNaN(installationId)) {
-      return NextResponse.json({ error: "Invalid installationId" }, { status: 400 });
+    const installationIdParam = url.searchParams.get("installationId");
+    const ownerParam = url.searchParams.get("owner");
+    const repoParam = url.searchParams.get("repo");
+
+    if (!installationIdParam || !ownerParam || !repoParam) {
+      return NextResponse.json(
+        { error: "Missing required parameters: installationId, owner, repo" },
+        { status: 400 }
+      );
     }
-    const owner = url.searchParams.get("owner") || "owner";
-    const repo = url.searchParams.get("repo") || "repo";
+
+    const installationId = parseInt(installationIdParam, 10);
+    if (isNaN(installationId) || installationId <= 0) {
+      return NextResponse.json({ error: "Invalid installationId" }, { status: 400 });
+    }
+
+    const owner = ownerParam;
+    const repo = repoParam;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const installationId = parseInt(url.searchParams.get("installationId") || "1", 10);
if (isNaN(installationId)) {
return NextResponse.json({ error: "Invalid installationId" }, { status: 400 });
}
const owner = url.searchParams.get("owner") || "owner";
const repo = url.searchParams.get("repo") || "repo";
const installationIdParam = url.searchParams.get("installationId");
const ownerParam = url.searchParams.get("owner");
const repoParam = url.searchParams.get("repo");
if (!installationIdParam || !ownerParam || !repoParam) {
return NextResponse.json(
{ error: "Missing required parameters: installationId, owner, repo" },
{ status: 400 }
);
}
const installationId = parseInt(installationIdParam, 10);
if (isNaN(installationId) || installationId <= 0) {
return NextResponse.json({ error: "Invalid installationId" }, { status: 400 });
}
const owner = ownerParam;
const repo = repoParam;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/integrations/incidents/webhook/route.ts` around lines 33 - 38, The
code currently uses unsafe defaults for installationId, owner, and repo; remove
the fallback defaults and instead validate presence and correctness: for
installationId call url.searchParams.get("installationId") and if null or
parseInt(...,10) yields NaN return NextResponse.json({ error: "Missing or
invalid installationId" }, { status: 400 }); similarly require
url.searchParams.get("owner") and url.searchParams.get("repo") to be
non-null/non-empty and if missing return 400 (e.g. NextResponse.json({ error:
"Missing owner" }, { status: 400 }) and "Missing repo" respectively); update the
code paths referencing installationId, owner, and repo to use these validated
values (no default "1", "owner", or "repo").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GSSoC'26 Part of GirlScript Summer of Code 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Critical: Incident webhook accepts forged requests that can trigger rollback PR preparation

1 participant