Skip to content

fix : SSRF IPv6 handling, profile avatar SSRF validation, analysis job null check, and webhook signature-first rate limiting#2467

Open
tmdeveloper007 wants to merge 3 commits into
nisshchayarathi:mainfrom
tmdeveloper007:fix/security-hardening
Open

fix : SSRF IPv6 handling, profile avatar SSRF validation, analysis job null check, and webhook signature-first rate limiting#2467
tmdeveloper007 wants to merge 3 commits into
nisshchayarathi:mainfrom
tmdeveloper007:fix/security-hardening

Conversation

@tmdeveloper007

@tmdeveloper007 tmdeveloper007 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Four security and reliability fixes bundled into one PR:

1. SSRF Validator IPv6-Mapped Address Handling (lib/utils/ssrfValidator.ts)

The isPrivateIP function did not check IPv6-mapped IPv4 addresses (e.g. ::ffff:127.0.0.1). An attacker could bypass SSRF protection by using IPv6 notation for private IPs.

Fix: Added regex match for ::ffff:x.x.x.x format and validate the embedded IPv4 part.

2. Profile Avatar HTTP URL SSRF Validation (app/api/users/profile/route.ts)

HTTP avatar URLs were stored without SSRF validation, allowing attackers to probe internal services (e.g. AWS metadata at 169.254.169.254).
Fix: Added validateSafeUrl() check for HTTP avatar URLs before storing.

3. Analysis Job Null Next_Run_At (lib/services/analysisJobService.ts)

The job claiming SQL used WHERE next_run_at <= NOW() which returns NULL for NULL values in PostgreSQL, causing jobs with NULL next_run_at to never be picked up.
Fix: Use COALESCE(next_run_at, NOW()) <= NOW() to handle NULL values.

4. Webhook Rate Limit Before Signature (app/api/integrations/github/webhook/route.ts)

Rate limiting happened before signature verification, allowing attackers to exhaust the rate limit for legitimate IPs using forged requests. Also, DLQ write failure returned 202 OK.
Fix: Moved signature verification to step 1 (before rate limiting). Return 503 when DLQ write fails so GitHub retries.

Summary by CodeRabbit

  • Bug Fixes

    • Improved GitHub webhook signature verification ordering and error response handling for better reliability.
    • Added security validation for avatar URLs to prevent server-side request forgery (SSRF) attacks.
  • New Features

    • Enhanced private IP detection to include IPv6-mapped IPv4 addresses for improved security.
    • Background analysis jobs without scheduled times now process immediately instead of waiting.

…b null check, and webhook signature-first rate limiting

- lib/utils/ssrfValidator.ts: Handle IPv6-mapped IPv4 addresses (::ffff:x.x.x.x) to prevent SSRF bypass via IPv6 notation
- app/api/users/profile/route.ts: Add SSRF validation for HTTP avatar URLs to prevent probing internal services
- lib/services/analysisJobService.ts: Use COALESCE(next_run_at, NOW()) to handle NULL next_run_at in job claiming SQL
- app/api/integrations/github/webhook/route.ts: Move signature verification before rate limiting to prevent DoS; return 503 when DLQ write fails
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

@tmdeveloper007 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

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@tmdeveloper007, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 45 minutes and 45 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7584c671-f0d7-44d4-aa0f-4a9deeee5907

📥 Commits

Reviewing files that changed from the base of the PR and between 638326a and 6fcee5a.

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

Walkthrough

The PR reorders GitHub webhook signature verification to occur before rate limiting and returns 503 on DLQ write failure. It extends isPrivateIP to detect IPv6-mapped IPv4 addresses and applies validateSafeUrl to avatar HTTP URLs in the profile endpoint. A SQL COALESCE fix allows jobs with NULL next_run_at to be claimed, and a component import path is corrected.

Changes

Security Hardening and Bug Fixes

Layer / File(s) Summary
SSRF validator: IPv6-mapped IPv4 support
lib/utils/ssrfValidator.ts
isPrivateIP gains a new branch that matches ::ffff:a.b.c.d IPv6-mapped IPv4 addresses, extracts the embedded IPv4 octets, and applies the same private/loopback/link-local/broadcast range checks. Comment documentation updated to include ::ffff:0.0.0.0/8.
Avatar URL SSRF guard in profile PUT
app/api/users/profile/route.ts
Imports validateSafeUrl and calls it when avatar is an http/https URL; returns a 400 JSON error if the URL resolves to a private or untrusted network address.
GitHub webhook handler: signature-first flow and DLQ 503
app/api/integrations/github/webhook/route.ts
Raw body read and HMAC signature verification moved to Step 1; rate limiting demoted to Step 2. DLQ write failure now returns 503 instead of 202. Unsupported event response simplified to { ok: true, message: "Event type not handled" }.
Job claim SQL: NULL next_run_at eligibility fix
lib/services/analysisJobService.ts
Replaces a1.next_run_at <= NOW() with COALESCE(a1.next_run_at, NOW()) <= NOW() so jobs without a scheduled next_run_at are immediately claimable.
RepositoryInsights import path correction
src/components/repository/RepositoryInsights.tsx
Updates ContributorIssueRecommendations import from a sibling-relative (./...) to a parent-relative (../...) path.

Sequence Diagram(s)

sequenceDiagram
    participant GitHub
    participant WebhookRoute
    participant GithubWebhookVerifier
    participant RateLimiter
    participant DLQ

    GitHub->>WebhookRoute: POST webhook event
    WebhookRoute->>WebhookRoute: Read raw body
    WebhookRoute->>GithubWebhookVerifier: Verify HMAC signature
    alt Signature invalid
        GithubWebhookVerifier-->>WebhookRoute: failure
        WebhookRoute-->>GitHub: 401 Unauthorized
    else Signature valid
        GithubWebhookVerifier-->>WebhookRoute: ok
        WebhookRoute->>RateLimiter: checkRateLimit
        alt Rate limit fallback fails and DLQ write fails
            WebhookRoute->>DLQ: Persist to DLQ
            DLQ-->>WebhookRoute: error
            WebhookRoute-->>GitHub: 503 (retry)
        else Rate limit exceeded
            WebhookRoute-->>GitHub: 429 rateLimitResponse
        else Unsupported event type
            WebhookRoute-->>GitHub: 200 Event type not handled
        else Supported event
            WebhookRoute-->>GitHub: 202 Accepted
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • nisshchayarathi/gitverse-nextjs#246: Both PRs modify the GitHub webhook route handler — the retrieved PR centralizes no-store headers and invalid-signature exits, while this PR reorders signature verification before rate limiting and changes DLQ/error responses.
  • nisshchayarathi/gitverse-nextjs#1732: This PR directly extends the SSRF validator work introduced in the retrieved PR by adding IPv6-mapped IPv4 detection to isPrivateIP.
  • nisshchayarathi/gitverse-nextjs#1941: Both PRs build on validateSafeUrl-based SSRF blocking for avatar HTTP URL handling; this PR extends it with IPv6-mapped IPv4 coverage.

Suggested labels

bug, security, gssoc:approved, level:critical, critical, mentor:nisshchayarathi, GSSoC'26

Poem

🐇 Hoppity-hop through the webhook gate,
Signatures checked before rate — how great!
IPv6 tricks? No sneaking past me,
COALESCE ensures no job runs free late.
Import paths fixed with a flick of my paw,
The warren is safer, with nary a flaw! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes all four main fixes in the changeset: SSRF IPv6 handling, profile avatar SSRF validation, analysis job null check, and webhook signature-first rate limiting.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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.

@github-actions

Copy link
Copy Markdown

⚠️ GSSoC Quality Check Failed — PR #2467

Hi @tmdeveloper007! 👋 Your PR has been flagged by our automated GSSoC quality check.

Issues found:

  • 🔗 No linked issue — Every PR must be linked to an open issue. Add closes #<issue-number> or fixes #<issue-number> in your description so maintainers know what this PR resolves.

✅ How to fix this

  1. Read the issues listed above carefully
  2. Edit your PR title and description to address them
  3. Make sure your PR is linked to an open issue using closes #<issue-number>
  4. Make sure your changes are meaningful and solve a real problem

Once you've fixed these, a maintainer will review and remove the flag. If you believe this is a mistake, please comment below. 🙏

GSSoC'26 automation · Maintainer: @nisshchayarathi

@github-actions github-actions Bot added the gssoc:invalid GSSoC: Invalid contribution label Jun 23, 2026
@tmdeveloper007

Copy link
Copy Markdown
Contributor Author

CodeRabbit review: approved. Note: Vercel CI check is failing due to "Authorization required to deploy" - this is a Vercel account permission issue with the fork (not a code problem). CodeRabbit code review has passed. The code changes are ready to merge once Vercel authorization is configured for tmdeveloper007/gitverse-nextjs.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/integrations/github/webhook/route.ts (1)

69-80: 🩺 Stability & Availability | 🟠 Major

Add a payload-size guard before reading the raw body.

Line 69 buffers the entire request without any size limit before signature verification. GitHub webhooks have a 25 MB hard limit, and an oversized payload can still consume memory and CPU for HMAC validation before rejection. Add a pre-read size check on the content-length header and a post-read guard, matching the pattern used throughout the codebase and GitHub's own constraint.

🛡️ Suggested guard
+const MAX_GITHUB_WEBHOOK_BODY_BYTES = 25 * 1024 * 1024;
+
 export async function POST(request: NextRequest) {
+  const contentLength = request.headers.get("content-length");
+  if (contentLength) {
+    const bytes = Number(contentLength);
+    if (!Number.isFinite(bytes) || bytes > MAX_GITHUB_WEBHOOK_BODY_BYTES) {
+      return NextResponse.json({ error: "Payload too large" }, { status: 413 });
+    }
+  }
+
   const rawBody = await request.text();
+  if (Buffer.byteLength(rawBody, "utf8") > MAX_GITHUB_WEBHOOK_BODY_BYTES) {
+    return NextResponse.json({ error: "Payload too large" }, { status: 413 });
+  }
🤖 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/github/webhook/route.ts` around lines 69 - 80, The
rawBody assignment at line 69 reads the entire request body without any size
validation, which can lead to memory exhaustion on oversized payloads before
signature verification occurs. Add a pre-read size guard by checking the
content-length header from request.headers.get("content-length") against
GitHub's 25 MB limit and reject requests that exceed this threshold before
calling request.text(). Additionally, implement a post-read guard that validates
the actual size of rawBody after it is read to catch cases where content-length
is missing or invalid. This ensures oversized payloads are rejected efficiently
without consuming resources for HMAC validation.
🧹 Nitpick comments (1)
lib/services/analysisJobService.ts (1)

431-431: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Prefer a sargable NULL-aware predicate for next_run_at.

Line 431 is functionally correct, but COALESCE(a1.next_run_at, NOW()) <= NOW() may block efficient use of a normal index on next_run_at in this claim hot path. Prefer:
(a1.next_run_at IS NULL OR a1.next_run_at <= NOW()).

Suggested SQL change
-          WHERE COALESCE(a1.next_run_at, NOW()) <= NOW()
+          WHERE (a1.next_run_at IS NULL OR a1.next_run_at <= NOW())
🤖 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 `@lib/services/analysisJobService.ts` at line 431, Replace the non-sargable
COALESCE predicate in the WHERE clause at line 431 of the analysisJobService.ts
file. Change `COALESCE(a1.next_run_at, NOW()) <= NOW()` to `(a1.next_run_at IS
NULL OR a1.next_run_at <= NOW())` to allow the database query optimizer to
efficiently use an index on the next_run_at column, improving query performance
in this hot code path.
🤖 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/github/webhook/route.ts`:
- Around line 91-126: The rate limiting check via checkRateLimit is currently
applied to all valid GitHub webhook deliveries before filtering for supported
event types. This means unsupported events like label, milestone, and ping
consume rate limit quota and can exhaust RATE_LIMITS.GITHUB_WEBHOOK, causing
legitimate pull_request, issues, and push events to be rate-limited with 429
responses. Move the event type validation (the check that returns 200 if the
event is not pull_request, issues, or push) to execute before the checkRateLimit
call. This ensures unsupported events are filtered out without consuming any
rate limit quota.

In `@app/api/users/profile/route.ts`:
- Around line 377-382: The validateSafeUrl call for the avatar URL validation is
occurring after database mutations (deleteMany calls for Google account/session
management) have already been committed, which means a failed avatar validation
can leave the account in a partially corrupted state. Move the avatar validation
using validateSafeUrl to the beginning of the profile update logic, before any
database mutations are executed, to ensure all validations pass before any data
changes are persisted.

In `@lib/utils/ssrfValidator.ts`:
- Around line 21-40: The ipv6MappedMatch regex in the ssrfValidator function
only matches the compressed dotted-decimal format of IPv6-mapped IPv4 addresses
(::ffff:a.b.c.d), but other equivalent representations like
0:0:0:0:0:ffff:a.b.c.d or ::ffff:7f00:1 bypass this check. Refactor the code to
normalize or parse all IPv6-mapped IPv4 address formats first to extract the
underlying IPv4 address, then apply the existing private-range checking logic
(the range checks from lines 31-37) to the extracted IPv4 address instead of
only matching one specific IPv6 format. This ensures all equivalent IPv6-mapped
representations are properly validated against the private IP ranges.

---

Outside diff comments:
In `@app/api/integrations/github/webhook/route.ts`:
- Around line 69-80: The rawBody assignment at line 69 reads the entire request
body without any size validation, which can lead to memory exhaustion on
oversized payloads before signature verification occurs. Add a pre-read size
guard by checking the content-length header from
request.headers.get("content-length") against GitHub's 25 MB limit and reject
requests that exceed this threshold before calling request.text(). Additionally,
implement a post-read guard that validates the actual size of rawBody after it
is read to catch cases where content-length is missing or invalid. This ensures
oversized payloads are rejected efficiently without consuming resources for HMAC
validation.

---

Nitpick comments:
In `@lib/services/analysisJobService.ts`:
- Line 431: Replace the non-sargable COALESCE predicate in the WHERE clause at
line 431 of the analysisJobService.ts file. Change `COALESCE(a1.next_run_at,
NOW()) <= NOW()` to `(a1.next_run_at IS NULL OR a1.next_run_at <= NOW())` to
allow the database query optimizer to efficiently use an index on the
next_run_at column, improving query performance in this hot code path.
🪄 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: 6ae0eed9-c50c-44bc-af93-ecbd546747c6

📥 Commits

Reviewing files that changed from the base of the PR and between cd4e69b and 638326a.

📒 Files selected for processing (5)
  • app/api/integrations/github/webhook/route.ts
  • app/api/users/profile/route.ts
  • lib/services/analysisJobService.ts
  • lib/utils/ssrfValidator.ts
  • src/components/repository/RepositoryInsights.tsx

Comment on lines +91 to +126
* Step 2: Rate limiting
* Apply per-IP rate limits after signature is validated.
*/
const deliveryId = request.headers.get("x-github-delivery") || "";

if (event !== "pull_request" && event !== "issues" && event !== "push") {
return NextResponse.json(
{ ok: true, ignored: true, event },
{ status: 200 },
);
}

let payload: WebhookPayload;
try {
payload = JSON.parse(rawBody);
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const ip = getClientIp(request);
const rl = await checkRateLimit(ip, RATE_LIMITS.GITHUB_WEBHOOK);

const action = payload.action;

if (event === "pull_request") {
if (!shouldHandlePullRequestAction(action)) {
return NextResponse.json(
{ ok: true, ignored: true, action },
{ status: 200 },
);
}
if (payload.pull_request?.draft && action !== "ready_for_review") {
return NextResponse.json(
{ ok: true, ignored: true, reason: "draft" },
{ status: 200 },
);
}
} else if (event === "issues") {
if (!shouldHandleIssueAction(action)) {
return NextResponse.json(
{ ok: true, ignored: true, action },
{ status: 200 },
);
if (rl.fallbackFailed) {
console.error("[WebhookRoute] Rate limiters completely failed. DLQing webhook.");
try {
await prisma.webhookEvent.create({
data: {
event: event || "unknown",
payload: rawBody,
status: "dlq",
error: "Rate limiter and fallback completely failed",
},
});
} catch (e) {
console.error("[WebhookRoute] Failed to write to DLQ!", e);
// Return 503 so GitHub retries the webhook delivery.
return NextResponse.json({ error: "Webhook processing failed. Please retry." }, { status: 503 });
}
} else if (event === "push") {
// We accept all push events — no action filtering needed
return NextResponse.json({ ok: true, message: "Webhook accepted and queued to DLQ due to severe outages" }, { status: 202 });
}

/*
* ┌──────────────────────────────────────────────────────────┐
* │ 4. Bot filtering │
* │ Ignore events sent by GitHub bots (including our own │
* │ automation) to prevent feedback loops. │
* └──────────────────────────────────────────────────────────┘
*/
if (payload.sender?.type === "Bot") {
return NextResponse.json(
{ ok: true, ignored: true, reason: "bot" },
{ status: 200 },
);
}
if (!rl.allowed) return rateLimitResponse(rl, "Webhook rate limit exceeded");

/*
* ┌──────────────────────────────────────────────────────────┐
* │ 5. Field validation │
* │ Ensure the payload contains the minimum required │
* │ fields before proceeding to idempotency and enqueue. │
* └──────────────────────────────────────────────────────────┘
* Step 3: Event routing — only process events we handle.
* Unsupported event types (label, milestone, etc.) and non-material PR/issue
* actions get a silent 200.
*/
const owner = payload.repository?.owner?.login;
const repo = payload.repository?.name;
const number = payload.pull_request?.number || payload.issue?.number;
const installationId = payload.installation?.id;

if (!owner || !repo || (!number && event !== "push") || !installationId) {
return NextResponse.json(
{
error: "Missing required fields",
details: { owner, repo, number, installationId, event },
},
{ status: 400 },
);
}
const deliveryId = request.headers.get("x-github-delivery") || "";

/*
* ┌──────────────────────────────────────────────────────────┐
* │ 6. Redis-based idempotency │
* │ Atomically claim the deliveryId so concurrent │
* │ deliveries of the same webhook are deduplicated. │
* │ The lock is released if the enqueue fails. │
* └──────────────────────────────────────────────────────────┘
*/
let idempotencyKey: string | null = null;
if (deliveryId) {
idempotencyKey = generateWebhookKey(deliveryId, event || "unknown", action);
const acquired = await tryAcquireIdempotency(idempotencyKey);
if (!acquired) {
return NextResponse.json(
{ ok: true, ignored: true, reason: "duplicate_delivery" },
{ status: 200 },
);
}
if (event !== "pull_request" && event !== "issues" && event !== "push") {
return NextResponse.json({ ok: true, message: "Event type not handled" }, { status: 200 });

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Filter unsupported signed events before consuming webhook quota.

Lines 94-96 rate-limit every valid GitHub delivery, but Lines 125-126 later discard unsupported event types. If the webhook receives signed label, milestone, ping, or other ignored events, they can exhaust RATE_LIMITS.GITHUB_WEBHOOK and cause handled PR/issues/push deliveries to get 429s.

🔁 Suggested reorder
   /*
-   * Step 2: Rate limiting
-   * Apply per-IP rate limits after signature is validated.
+   * Step 2: Event routing — only process events we handle.
+   * Unsupported event types (label, milestone, etc.) get a silent 200
+   * without consuming processing quota.
    */
+  if (event !== "pull_request" && event !== "issues" && event !== "push") {
+    return NextResponse.json({ ok: true, message: "Event type not handled" }, { status: 200 });
+  }
+
+  /*
+   * Step 3: Rate limiting
+   * Apply per-IP rate limits after signature is validated and event is relevant.
+   */
   const ip = getClientIp(request);
   const rl = await checkRateLimit(ip, RATE_LIMITS.GITHUB_WEBHOOK);
 
@@
   if (!rl.allowed) return rateLimitResponse(rl, "Webhook rate limit exceeded");
 
   /*
-   * Step 3: Event routing — only process events we handle.
-   * Unsupported event types (label, milestone, etc.) and non-material PR/issue
-   * actions get a silent 200.
+   * Step 4: Event processing
+   * Non-material PR/issue actions get a silent 200.
    */
   const deliveryId = request.headers.get("x-github-delivery") || "";
-
-  if (event !== "pull_request" && event !== "issues" && event !== "push") {
-    return NextResponse.json({ ok: true, message: "Event type not handled" }, { status: 200 });
-  }
📝 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
* Step 2: Rate limiting
* Apply per-IP rate limits after signature is validated.
*/
const deliveryId = request.headers.get("x-github-delivery") || "";
if (event !== "pull_request" && event !== "issues" && event !== "push") {
return NextResponse.json(
{ ok: true, ignored: true, event },
{ status: 200 },
);
}
let payload: WebhookPayload;
try {
payload = JSON.parse(rawBody);
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const ip = getClientIp(request);
const rl = await checkRateLimit(ip, RATE_LIMITS.GITHUB_WEBHOOK);
const action = payload.action;
if (event === "pull_request") {
if (!shouldHandlePullRequestAction(action)) {
return NextResponse.json(
{ ok: true, ignored: true, action },
{ status: 200 },
);
}
if (payload.pull_request?.draft && action !== "ready_for_review") {
return NextResponse.json(
{ ok: true, ignored: true, reason: "draft" },
{ status: 200 },
);
}
} else if (event === "issues") {
if (!shouldHandleIssueAction(action)) {
return NextResponse.json(
{ ok: true, ignored: true, action },
{ status: 200 },
);
if (rl.fallbackFailed) {
console.error("[WebhookRoute] Rate limiters completely failed. DLQing webhook.");
try {
await prisma.webhookEvent.create({
data: {
event: event || "unknown",
payload: rawBody,
status: "dlq",
error: "Rate limiter and fallback completely failed",
},
});
} catch (e) {
console.error("[WebhookRoute] Failed to write to DLQ!", e);
// Return 503 so GitHub retries the webhook delivery.
return NextResponse.json({ error: "Webhook processing failed. Please retry." }, { status: 503 });
}
} else if (event === "push") {
// We accept all push events — no action filtering needed
return NextResponse.json({ ok: true, message: "Webhook accepted and queued to DLQ due to severe outages" }, { status: 202 });
}
/*
* ┌──────────────────────────────────────────────────────────┐
* 4. Bot filtering
* Ignore events sent by GitHub bots (including our own
* automation) to prevent feedback loops.
* └──────────────────────────────────────────────────────────┘
*/
if (payload.sender?.type === "Bot") {
return NextResponse.json(
{ ok: true, ignored: true, reason: "bot" },
{ status: 200 },
);
}
if (!rl.allowed) return rateLimitResponse(rl, "Webhook rate limit exceeded");
/*
* ┌──────────────────────────────────────────────────────────┐
* 5. Field validation
* Ensure the payload contains the minimum required
* fields before proceeding to idempotency and enqueue.
* └──────────────────────────────────────────────────────────┘
* Step 3: Event routing only process events we handle.
* Unsupported event types (label, milestone, etc.) and non-material PR/issue
* actions get a silent 200.
*/
const owner = payload.repository?.owner?.login;
const repo = payload.repository?.name;
const number = payload.pull_request?.number || payload.issue?.number;
const installationId = payload.installation?.id;
if (!owner || !repo || (!number && event !== "push") || !installationId) {
return NextResponse.json(
{
error: "Missing required fields",
details: { owner, repo, number, installationId, event },
},
{ status: 400 },
);
}
const deliveryId = request.headers.get("x-github-delivery") || "";
/*
* ┌──────────────────────────────────────────────────────────┐
* 6. Redis-based idempotency
* Atomically claim the deliveryId so concurrent
* deliveries of the same webhook are deduplicated.
* The lock is released if the enqueue fails.
* └──────────────────────────────────────────────────────────┘
*/
let idempotencyKey: string | null = null;
if (deliveryId) {
idempotencyKey = generateWebhookKey(deliveryId, event || "unknown", action);
const acquired = await tryAcquireIdempotency(idempotencyKey);
if (!acquired) {
return NextResponse.json(
{ ok: true, ignored: true, reason: "duplicate_delivery" },
{ status: 200 },
);
}
if (event !== "pull_request" && event !== "issues" && event !== "push") {
return NextResponse.json({ ok: true, message: "Event type not handled" }, { status: 200 });
/*
* Step 2: Event routing only process events we handle.
* Unsupported event types (label, milestone, etc.) get a silent 200
* without consuming processing quota.
*/
if (event !== "pull_request" && event !== "issues" && event !== "push") {
return NextResponse.json({ ok: true, message: "Event type not handled" }, { status: 200 });
}
/*
* Step 3: Rate limiting
* Apply per-IP rate limits after signature is validated and event is relevant.
*/
const ip = getClientIp(request);
const rl = await checkRateLimit(ip, RATE_LIMITS.GITHUB_WEBHOOK);
if (rl.fallbackFailed) {
console.error("[WebhookRoute] Rate limiters completely failed. DLQing webhook.");
try {
await prisma.webhookEvent.create({
data: {
event: event || "unknown",
payload: rawBody,
status: "dlq",
error: "Rate limiter and fallback completely failed",
},
});
} catch (e) {
console.error("[WebhookRoute] Failed to write to DLQ!", e);
// Return 503 so GitHub retries the webhook delivery.
return NextResponse.json({ error: "Webhook processing failed. Please retry." }, { status: 503 });
}
return NextResponse.json({ ok: true, message: "Webhook accepted and queued to DLQ due to severe outages" }, { status: 202 });
}
if (!rl.allowed) return rateLimitResponse(rl, "Webhook rate limit exceeded");
/*
* Step 4: Event processing
* Non-material PR/issue actions get a silent 200.
*/
const deliveryId = request.headers.get("x-github-delivery") || "";
🤖 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/github/webhook/route.ts` around lines 91 - 126, The rate
limiting check via checkRateLimit is currently applied to all valid GitHub
webhook deliveries before filtering for supported event types. This means
unsupported events like label, milestone, and ping consume rate limit quota and
can exhaust RATE_LIMITS.GITHUB_WEBHOOK, causing legitimate pull_request, issues,
and push events to be rate-limited with 429 responses. Move the event type
validation (the check that returns 200 if the event is not pull_request, issues,
or push) to execute before the checkRateLimit call. This ensures unsupported
events are filtered out without consuming any rate limit quota.

Comment on lines +377 to +382
const isSafe = await validateSafeUrl(avatar);
if (!isSafe) {
return NextResponse.json(
{ error: "Avatar URL resolves to an untrusted or private network address." },
{ status: 400 }
);

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Move this validation before any profile-update mutations.

Line 379 can return 400 after the earlier Google-account/session deleteMany calls have already committed for email-changing linked accounts. An unsafe avatar URL can therefore partially unlink/invalidate the account while the profile update fails. Run all avatar validation before those DB mutations, or make the whole update flow transactional.

🤖 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/users/profile/route.ts` around lines 377 - 382, The validateSafeUrl
call for the avatar URL validation is occurring after database mutations
(deleteMany calls for Google account/session management) have already been
committed, which means a failed avatar validation can leave the account in a
partially corrupted state. Move the avatar validation using validateSafeUrl to
the beginning of the profile update logic, before any database mutations are
executed, to ensure all validations pass before any data changes are persisted.

Comment on lines +21 to +40
const ipv6MappedMatch = ip.match(/^::ffff:(\d+)\.(\d+)\.(\d+)\.(\d+)$/i);
if (ipv6MappedMatch) {
const parts = [
parseInt(ipv6MappedMatch[1], 10),
parseInt(ipv6MappedMatch[2], 10),
parseInt(ipv6MappedMatch[3], 10),
parseInt(ipv6MappedMatch[4], 10),
];

if (
parts[0] === 10 || // 10.0.0.0/8
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12
(parts[0] === 192 && parts[1] === 168) || // 192.168.0.0/16
parts[0] === 127 || // 127.0.0.0/8
(parts[0] === 169 && parts[1] === 254) || // 169.254.0.0/16
parts[0] === 0 // 0.0.0.0/8
) {
return true;
}
}

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.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Normalize all IPv6-mapped IPv4 forms before checking ranges.

Line 21 only matches compressed dotted-decimal ::ffff:a.b.c.d. Equivalent mapped forms like 0:0:0:0:0:ffff:127.0.0.1 or ::ffff:7f00:1 still fall through and are treated as public, so private/loopback IPv4 can bypass this validator. Prefer parsing/canonicalizing mapped IPv6 addresses and then reusing one IPv4 private-range predicate.

🤖 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 `@lib/utils/ssrfValidator.ts` around lines 21 - 40, The ipv6MappedMatch regex
in the ssrfValidator function only matches the compressed dotted-decimal format
of IPv6-mapped IPv4 addresses (::ffff:a.b.c.d), but other equivalent
representations like 0:0:0:0:0:ffff:a.b.c.d or ::ffff:7f00:1 bypass this check.
Refactor the code to normalize or parse all IPv6-mapped IPv4 address formats
first to extract the underlying IPv4 address, then apply the existing
private-range checking logic (the range checks from lines 31-37) to the
extracted IPv4 address instead of only matching one specific IPv6 format. This
ensures all equivalent IPv6-mapped representations are properly validated
against the private IP ranges.

@tmdeveloper007

Copy link
Copy Markdown
Contributor Author

PR updated: restored webhook processing logic (bot filtering, field validation, idempotency) + fixed ESLint unused imports. Closes #2469.

@tmdeveloper007

Copy link
Copy Markdown
Contributor Author

CI Status: Playwright Tests PASSED, CodeQL PASSED, Prisma Schema Check PASSED. Test Platform and Worker Consistency failures are pre-existing upstream issues unrelated to this PR.

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

Labels

gssoc:invalid GSSoC: Invalid contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant