Skip to content

Conversation

@agarwal-tanmay-work
Copy link

@agarwal-tanmay-work agarwal-tanmay-work commented Dec 26, 2025

What was happening earlier

The GitHub webhook handler processes incoming webhook payloads without explicitly
verifying the x-hub-signature-256 header against a shared secret.

If the webhook endpoint is publicly accessible, a forged request could trigger
webhook-driven workflows (e.g. review agents) without originating from GitHub.

Resolved

Implemented the GitHub webhook signature verification.
The handler now extracts the raw request body and verifies it against the x-hub-signature-256 header using the GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET. This prevents forged payloads from triggering the review agent.

Summary by CodeRabbit

  • New Features
    • Added signature verification for webhooks to ensure requests originate from trusted sources.

✏️ Tip: You can customize this high-level summary in your review settings.

Implemented the GitHub webhook signature verification
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

Walkthrough

Adds HMAC signature verification to the GitHub webhook endpoint using SHA-256 hashing and timing-safe comparison. The webhook handler now reads request body as text, extracts the signature header, validates it against the payload, and rejects requests with invalid or missing signatures before processing events.

Changes

Cohort / File(s) Summary
GitHub Webhook Security
packages/web/src/app/api/(server)/webhook/route.ts
Added verifyGitHubSignature() function implementing SHA-256 HMAC verification with timing-safe comparison. Refactored request handling to read raw body text, extract x-hub-signature-256 header, validate signature before JSON parsing, and return 401 Unauthorized for invalid/missing signatures. Existing pull_request and issue_comment event handlers remain unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Pre-merge checks and finishing touches

✅ 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 directly and clearly describes the main change: adding webhook signature verification for x-hub-signature-256 header validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@brendan-kellam
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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

🧹 Nitpick comments (1)
packages/web/src/app/api/(server)/webhook/route.ts (1)

107-118: Consider wrapping webhook processing in try-catch for comprehensive error handling.

Based on the Stripe webhook handler pattern (in packages/web/src/app/api/(server)/stripe/route.ts), consider wrapping the entire webhook processing logic in a try-catch block. This ensures any unexpected errors are caught and logged appropriately, preventing unhandled exceptions from crashing the handler.

🔎 Example pattern from Stripe handler
export const POST = async (request: NextRequest) => {
    try {
        const rawBody = await request.text();
        // ... signature verification and processing ...
        return Response.json({ status: 'ok' });
    } catch (error) {
        logger.error('Error processing webhook', error);
        return new Response('Internal Server Error', { status: 500 });
    }
};
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f757a5b and cb96760.

📒 Files selected for processing (1)
  • packages/web/src/app/api/(server)/webhook/route.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/web/src/app/api/(server)/webhook/route.ts (1)
packages/web/src/app/api/(server)/stripe/route.ts (1)
  • POST (12-105)
🔇 Additional comments (1)
packages/web/src/app/api/(server)/webhook/route.ts (1)

5-5: LGTM! Necessary imports for webhook signature verification.

The crypto import provides HMAC functionality for signature verification, and WebhookEventDefinition supports the existing type guards.

Also applies to: 13-13

Comment on lines +100 to +104
const verifyGitHubSignature = (payload: string, signature: string, secret: string): boolean => {
const hmac = crypto.createHmac('sha256', secret);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Add error handling for length mismatch in timingSafeEqual.

crypto.timingSafeEqual throws an error if the two buffers have different lengths. If an attacker or a malformed webhook sends a signature with unexpected length, this function will throw an unhandled exception, causing a 500 error instead of properly rejecting the request.

🔎 Proposed fix with try-catch and length validation
 const verifyGitHubSignature = (payload: string, signature: string, secret: string): boolean => {
+    try {
         const hmac = crypto.createHmac('sha256', secret);
         const digest = 'sha256=' + hmac.update(payload).digest('hex');
+        if (digest.length !== signature.length) {
+            return false;
+        }
         return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
+    } catch (error) {
+        logger.warn('Error verifying GitHub signature', error);
+        return false;
+    }
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/web/src/app/api/(server)/webhook/route.ts around lines 100 to 104,
the verifyGitHubSignature function currently calls crypto.timingSafeEqual
without guarding against differing buffer lengths which causes it to throw;
update the function to first validate the signature format (ensure it starts
with "sha256="), compute the expected digest, convert both expected and provided
signatures to Buffers, immediately return false if their lengths differ, and
wrap the timingSafeEqual call in a try/catch that returns false on any error so
malformed or malicious signatures never cause an exception to bubble up.

return new Response('Unauthorized', { status: 401 });
}

const body = JSON.parse(rawBody);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Add error handling for JSON parsing.

JSON.parse can throw an error if the request body is not valid JSON. An attacker could send a malformed JSON payload with a valid signature (or if signature verification fails elsewhere), causing an unhandled exception.

🔎 Proposed fix with try-catch
+    let body;
+    try {
-        const body = JSON.parse(rawBody);
+        body = JSON.parse(rawBody);
+    } catch (error) {
+        logger.warn('Failed to parse webhook body as JSON', error);
+        return new Response('Bad Request', { status: 400 });
+    }
📝 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 body = JSON.parse(rawBody);
let body;
try {
body = JSON.parse(rawBody);
} catch (error) {
logger.warn('Failed to parse webhook body as JSON', error);
return new Response('Bad Request', { status: 400 });
}
🤖 Prompt for AI Agents
In packages/web/src/app/api/(server)/webhook/route.ts around line 118,
JSON.parse(rawBody) can throw on malformed input; wrap the parse in a try-catch,
catch the error, log the parsing error (with context), and return a proper HTTP
response (400 Bad Request) or similar error response instead of letting the
exception bubble; ensure subsequent logic (signature verification/processing)
only runs when parsing succeeds.

const verifyGitHubSignature = (payload: string, signature: string, secret: string): boolean => {
const hmac = crypto.createHmac('sha256', secret);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
Copy link
Contributor

Choose a reason for hiding this comment

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

should probably catch in here and return false if it throws

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants