Skip to content

🛡️ Sentinel: [HIGH] Fix SSRF loopback vulnerability in bulk lookup API#150

Open
aicoder2009 wants to merge 1 commit into
mainfrom
fix/ssrf-bulk-lookup-1604677428229134358
Open

🛡️ Sentinel: [HIGH] Fix SSRF loopback vulnerability in bulk lookup API#150
aicoder2009 wants to merge 1 commit into
mainfrom
fix/ssrf-bulk-lookup-1604677428229134358

Conversation

@aicoder2009

@aicoder2009 aicoder2009 commented Jun 7, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: HIGH
💡 Vulnerability: The bulk API endpoint (src/app/api/lookup/bulk/route.ts) was using request.nextUrl.origin to construct an absolute URL for a fetch call to other internal APIs. Because request.nextUrl.origin is derived from the HTTP Host header sent by the client, an attacker could spoof the Host header to cause the server to make requests to arbitrary external servers or internal IP addresses (SSRF).
🎯 Impact: This could be exploited to bypass security controls, interact with internal networks, or launch DoS attacks.
🔧 Fix: Instead of using fetch to make a loopback HTTP request, the code now imports the other route handler functions (e.g., POST from ../url/route) directly and invokes them as normal asynchronous function calls. A synthetic NextRequest is passed to satisfy the handler's parameters.
✅ Verification: Ran pnpm test:run to ensure the bulk API works securely and passes all tests.


PR created automatically by Jules for task 1604677428229134358 started by @aicoder2009

Summary by CodeRabbit

  • Bug Fixes

    • Fixed a Server-Side Request Forgery (SSRF) vulnerability in the bulk API route that could be exploited through manipulated host headers.
  • Refactor

    • Enhanced internal architecture of the bulk lookup API for improved reliability and maintainability.

Directly invoke route handlers instead of performing loopback HTTP requests using `request.nextUrl.origin`, mitigating potential SSRF vulnerabilities.

Co-authored-by: aicoder2009 <127642633+aicoder2009@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI review requested due to automatic review settings June 7, 2026 06:17
@vercel

vercel Bot commented Jun 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opencitation Ready Ready Preview, Comment Jun 7, 2026 6:18am

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR remediates an SSRF vulnerability in the bulk lookup route by replacing fetch-based internal API calls (which derived the origin from the client's Host header) with direct handler invocation. The implementation now imports sub-route handlers and executes them via synthetic NextRequest objects, with tests updated to mock handlers instead of global fetch.

Changes

SSRF Vulnerability Remediation

Layer / File(s) Summary
Security vulnerability documentation
.jules/sentinel.md
Documents the SSRF vulnerability caused by using request.nextUrl.origin for internal loopback fetch calls, and records the mitigation approach of direct handler invocation with synthetic NextRequest.
Direct handler invocation implementation
src/app/api/lookup/bulk/route.ts
Imports URL/DOI/ISBN handlers and refactors POST logic to detect input type, select the appropriate handler and payload, then invoke the handler directly via a synthetic NextRequest targeting http://localhost. Removes the prior fetch-based endpoint path.
Test refactoring for mocked handlers
src/app/api/lookup/bulk/route.test.ts
Replaces global.fetch mocking with mocked imports of sub-route handlers. Updates routing tests to assert handler invocation via toHaveBeenCalled instead of inspecting fetch URLs. Adjusts mock sequences and failure scenarios to use handler return values.

Sequence Diagram

sequenceDiagram
  participant Client
  participant BulkRoute as POST /bulk
  participant URLHandler as URL Handler
  participant DOIHandler as DOI Handler
  Client->>BulkRoute: POST bulk request (items)
  loop For each item
    alt URL item
      BulkRoute->>BulkRoute: Select URLHandler, create payload
      BulkRoute->>URLHandler: invoke with NextRequest
      URLHandler-->>BulkRoute: NextResponse
    else DOI item
      BulkRoute->>BulkRoute: Select DOIHandler, create payload
      BulkRoute->>DOIHandler: invoke with NextRequest
      DOIHandler-->>BulkRoute: NextResponse
    end
  end
  BulkRoute-->>Client: bulk results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops past the loopback trap,
No more Host-header mishaps in our app!
Direct handlers called, no fetch detours,
The SSRF vulnerability now cures. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing an SSRF vulnerability in the bulk lookup API. It is specific, actionable, and directly reflects the core security fix in the changeset.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ssrf-bulk-lookup-1604677428229134358

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/app/api/lookup/bulk/route.ts (1)

30-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate item type before calling trim() to prevent batch-wide 500s.

Line 31 assumes every entry is a string. A payload like { items: [123] } throws before per-item error handling, rejects Promise.all, and returns a 500 for the entire request.

🛠️ Suggested fix
     const lookupPromises = items.map(async (item) => {
+      if (typeof item !== "string") {
+        return {
+          input: String(item),
+          success: false,
+          error: "Each item must be a string"
+        };
+      }
+
       const trimmedItem = item.trim();
       if (!trimmedItem) {
         return { input: item, success: false, error: "Empty input" };
       }
🤖 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 `@src/app/api/lookup/bulk/route.ts` around lines 30 - 33, The map callback for
lookupPromises calls item.trim() without validating the type, which can throw
for non-strings and cause Promise.all to reject; update the items.map async
callback (the function producing lookupPromises) to first check typeof item ===
"string" (or safely coerce with String(item) if desired) and if it's not a
string immediately return the per-item error shape ({ input: item, success:
false, error: "Invalid input type" }) instead of calling trim(); ensure the
branch that calls trim() only runs for string values so individual bad items
produce per-item errors rather than crashing the whole batch.
🧹 Nitpick comments (1)
src/app/api/lookup/bulk/route.test.ts (1)

61-133: ⚡ Quick win

Add a dedicated hostile-origin regression test for the SSRF fix.

Current tests validate routing outcomes, but none explicitly lock in that a malicious request origin/Host cannot influence internal dispatch again.

✅ Suggested test addition
+  it('ignores request origin when dispatching internal lookups', async () => {
+    (urlPOST as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
+      NextResponse.json({ data: { title: 'Example Page' } }, { status: 200 })
+    );
+
+    const request = new NextRequest('http://attacker.example/api/lookup/bulk', {
+      method: 'POST',
+      body: JSON.stringify({ items: ['https://example.com'] }),
+      headers: { host: 'attacker.example' },
+    });
+
+    const response = await POST(request);
+    const data = await response.json();
+
+    expect(response.status).toBe(200);
+    expect(data.results[0].success).toBe(true);
+    expect(urlPOST).toHaveBeenCalledTimes(1);
+  });

As per coding guidelines, src/app/api/**/*.ts: Write tests for API route logic.

🤖 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 `@src/app/api/lookup/bulk/route.test.ts` around lines 61 - 133, Tests exercise
routing but lack a regression test ensuring a hostile Host/origin header cannot
influence dispatch; add a new test in route.test.ts that constructs a request
via makeRequest with items (e.g., a URL and a DOI) while setting a malicious
Host/Origin header, then calls POST and asserts routing still calls the correct
handlers (urlPOST, doiPOST, isbnPOST as appropriate), that results reflect the
mocked responses, and that no routing decision depends on the Host header (e.g.,
urlPOST/doiPOST are invoked regardless of the hostile header) to lock in the
SSRF fix.

Source: Coding guidelines

🤖 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 @.jules/sentinel.md:
- Line 10: The header line "## 2024-06-07 - SSRF via Host-header in Next.js
Loopback APIs" has the wrong year; update that date string to "2026-06-07"
(matching the PR created on June 7, 2026) keeping the same header format so it
reads "## 2026-06-07 - SSRF via Host-header in Next.js Loopback APIs".

---

Outside diff comments:
In `@src/app/api/lookup/bulk/route.ts`:
- Around line 30-33: The map callback for lookupPromises calls item.trim()
without validating the type, which can throw for non-strings and cause
Promise.all to reject; update the items.map async callback (the function
producing lookupPromises) to first check typeof item === "string" (or safely
coerce with String(item) if desired) and if it's not a string immediately return
the per-item error shape ({ input: item, success: false, error: "Invalid input
type" }) instead of calling trim(); ensure the branch that calls trim() only
runs for string values so individual bad items produce per-item errors rather
than crashing the whole batch.

---

Nitpick comments:
In `@src/app/api/lookup/bulk/route.test.ts`:
- Around line 61-133: Tests exercise routing but lack a regression test ensuring
a hostile Host/origin header cannot influence dispatch; add a new test in
route.test.ts that constructs a request via makeRequest with items (e.g., a URL
and a DOI) while setting a malicious Host/Origin header, then calls POST and
asserts routing still calls the correct handlers (urlPOST, doiPOST, isbnPOST as
appropriate), that results reflect the mocked responses, and that no routing
decision depends on the Host header (e.g., urlPOST/doiPOST are invoked
regardless of the hostile header) to lock in the SSRF fix.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c113b861-d6f7-48d2-9e83-18c0a7785c0f

📥 Commits

Reviewing files that changed from the base of the PR and between b872925 and 2eeedca.

📒 Files selected for processing (3)
  • .jules/sentinel.md
  • src/app/api/lookup/bulk/route.test.ts
  • src/app/api/lookup/bulk/route.ts

Comment thread .jules/sentinel.md
**Learning:** `marked` does not sanitize HTML by default. While this may seem safe for trusted inputs (like internal docs or GitHub releases), if malicious input manages to enter these sources, it leads directly to an XSS vulnerability.
**Prevention:** The output of `marked` (or any markdown parser) must always be wrapped with `DOMPurify.sanitize()` (using `isomorphic-dompurify` for SSR) before being passed to `dangerouslySetInnerHTML`.

## 2024-06-07 - SSRF via Host-header in Next.js Loopback APIs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct the SSRF note date to match the remediation timeline.

Line 10 uses 2024-06-07, but this fix is tracked in a PR created on June 7, 2026. Keeping the date aligned avoids audit/incident confusion.

✏️ Suggested edit
-## 2024-06-07 - SSRF via Host-header in Next.js Loopback APIs
+## 2026-06-07 - SSRF via Host-header in Next.js Loopback APIs
📝 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
## 2024-06-07 - SSRF via Host-header in Next.js Loopback APIs
## 2026-06-07 - SSRF via Host-header in Next.js Loopback APIs
🤖 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 @.jules/sentinel.md at line 10, The header line "## 2024-06-07 - SSRF via
Host-header in Next.js Loopback APIs" has the wrong year; update that date
string to "2026-06-07" (matching the PR created on June 7, 2026) keeping the
same header format so it reads "## 2026-06-07 - SSRF via Host-header in Next.js
Loopback APIs".

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.

2 participants