Skip to content

🛡️ Sentinel: [CRITICAL] Fix SSRF in bulk lookup API#195

Open
aicoder2009 wants to merge 1 commit into
mainfrom
sentinel/ssrf-bulk-lookup-7184815631816819964
Open

🛡️ Sentinel: [CRITICAL] Fix SSRF in bulk lookup API#195
aicoder2009 wants to merge 1 commit into
mainfrom
sentinel/ssrf-bulk-lookup-7184815631816819964

Conversation

@aicoder2009

@aicoder2009 aicoder2009 commented Jun 28, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: CRITICAL
💡 Vulnerability: The application was vulnerable to Server-Side Request Forgery (SSRF) in the bulk lookup API (src/app/api/lookup/bulk/route.ts). Loopback fetch calls were constructed dynamically using request.nextUrl.origin, which relies on the client-controlled Host header. An attacker could manipulate this header to force the internal API to make requests to an arbitrary external server.
🎯 Impact: An attacker could perform network scanning or access sensitive internal services via the server, potentially exposing metadata or bypassing external firewall restrictions.
🔧 Fix: Replaced the loopback fetch mechanism with direct invocations of the corresponding Next.js Route Handlers (urlLookup, doiLookup, isbnLookup), using a hardcoded, synthetic internal URL request object. This entirely bypasses the network layer and ignores external Host headers, inherently eliminating the SSRF. Updated Vitest suite appropriately.
Verification: Verify that pnpm test:run passes. The bulk/route.test.ts file now asserts against direct handler calls using vi.mock instead of mocking global.fetch.


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

Summary by CodeRabbit

  • Security Fixes

    • Hardened bulk lookup requests to prevent server-side request forgery from manipulated host headers.
    • Internal lookups now use a safer direct route path, reducing the risk of unintended network loopbacks.
  • Tests

    • Updated coverage to verify bulk lookup routing behavior through the individual lookup handlers.
    • Adjusted success and failure scenarios to match the new request flow.

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.

@vercel

vercel Bot commented Jun 28, 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 28, 2026 6:29am

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The bulk lookup API route is refactored to import and directly invoke the individual lookup sub-route handlers (URL, DOI, ISBN) using a synthetic NextRequest with a fixed http://localhost origin, replacing the prior fetch-based internal HTTP calls that used the user-controlled Host header. Tests are updated to mock the handler modules instead of global.fetch, and a sentinel doc entry records the SSRF vulnerability and remediation.

Changes

SSRF Fix: Direct Handler Dispatch in Bulk Lookup

Layer / File(s) Summary
Direct handler dispatch replacing fetch calls
src/app/api/lookup/bulk/route.ts
Adds imports of sub-route POST handlers; refactors per-item processing to select a handler by regex and invoke it with a synthetic NextRequest at a fixed http://localhost origin instead of constructing a fetch URL from request.nextUrl.origin.
Test suite updated to mock handler modules
src/app/api/lookup/bulk/route.test.ts
Replaces global.fetch mocking with vi.mock stubs for the three lookup route modules; updates routing assertions to check handler calls and wires failure/success paths via NextResponse.json-based mock returns.
Sentinel doc for SSRF vulnerability
.jules/sentinel.md
Adds a dated entry documenting the SSRF vulnerability caused by using request.nextUrl.origin for internal loopback fetches and the remediation steps.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 No more sneaky host header tricks,
The rabbit patched the loopback fix!
Direct dispatch, synthetic request,
localhost locked — security blessed.
Hops forward, no SSRF stress! 🎉

🚥 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 and accurately summarizes the main change: fixing an SSRF issue in the bulk lookup API.
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.
✨ 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 sentinel/ssrf-bulk-lookup-7184815631816819964

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.

@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: 2

🧹 Nitpick comments (1)
src/app/api/lookup/bulk/route.test.ts (1)

64-85: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert the synthetic request contract, not just that a handler was called.

These tests would be stronger if they passed a hostile incoming origin and asserted the mocked handler receives http://localhost plus the expected JSON body.

Example test shape
-    (urlLookup as ReturnType<typeof vi.fn>).mockResolvedValueOnce(NextResponse.json({ data: { title: 'Example Page' } }, { status: 200 }));
-    const response = await POST(makeRequest({ items: ['https://example.com'] }));
+    (urlLookup as ReturnType<typeof vi.fn>).mockImplementationOnce(async (req: NextRequest) => {
+      expect(req.nextUrl.origin).toBe('http://localhost');
+      await expect(req.json()).resolves.toEqual({ url: 'https://example.com' });
+      return NextResponse.json({ data: { title: 'Example Page' } }, { status: 200 });
+    });
+    const response = await POST(new NextRequest('http://attacker.test/api/lookup/bulk', {
+      method: 'POST',
+      body: JSON.stringify({ items: ['https://example.com'] }),
+    }));

As per coding guidelines, "**/{src/lib/citation/**/*.test.ts,src/app/api/**/*.test.ts}: Write tests for citation formatting and 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 64 - 85, The bulk lookup
route tests only verify that the lookup handlers are called, but they should
also assert the synthetic request contract used by POST in route.test.ts. Update
the tests around POST/makeRequest so they pass a hostile incoming origin and
confirm the mocked handler receives a request with http://localhost as the
origin plus the expected JSON body for each item type. Use the existing symbols
POST, makeRequest, urlLookup, doiLookup, and isbnLookup to locate the assertions
and strengthen them to validate the forwarded request shape, not just
invocation.

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 `@src/app/api/lookup/bulk/route.ts`:
- Around line 40-42: The bulk lookup URL branch in the route handler accepts
values starting with www. but passes them directly to urlLookup, which later
parses them with new URL and fails. Update the URL-handling logic in the bulk
route to normalize www. inputs by prefixing https:// before assigning the body,
while keeping existing http/https URLs unchanged.
- Around line 28-29: The bulk lookup handler in route.ts should validate each
element in items before calling trim() inside lookupPromises. Update the item
processing logic to reject non-string entries early, returning a client error
instead of letting item.trim() throw and bubble into a 500. Use the existing
bulk request flow around lookupPromises and the item handling path to add a
per-item type check before trimming and lookup.

---

Nitpick comments:
In `@src/app/api/lookup/bulk/route.test.ts`:
- Around line 64-85: The bulk lookup route tests only verify that the lookup
handlers are called, but they should also assert the synthetic request contract
used by POST in route.test.ts. Update the tests around POST/makeRequest so they
pass a hostile incoming origin and confirm the mocked handler receives a request
with http://localhost as the origin plus the expected JSON body for each item
type. Use the existing symbols POST, makeRequest, urlLookup, doiLookup, and
isbnLookup to locate the assertions and strengthen them to validate the
forwarded request shape, not just invocation.
🪄 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: e49eb0a4-1643-4a26-a78a-6b385c0d2022

📥 Commits

Reviewing files that changed from the base of the PR and between b69285b and bedc23d.

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

Comment on lines 28 to 29
const lookupPromises = items.map(async (item) => {
const trimmedItem = item.trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate item element types before trimming.

items is only checked as an array, so a payload like { "items": [123] } makes item.trim() reject and turns the whole bulk request into a 500.

Proposed fix
     if (!items || !Array.isArray(items) || items.length === 0) {
       return NextResponse.json({ error: "Items array is required" }, { status: 400 });
     }
+
+    if (!items.every((item): item is string => typeof item === "string")) {
+      return NextResponse.json({ error: "All items must be strings" }, { 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 lookupPromises = items.map(async (item) => {
const trimmedItem = item.trim();
if (!items.every((item): item is string => typeof item === "string")) {
return NextResponse.json({ error: "All items must be strings" }, { status: 400 });
}
const lookupPromises = items.map(async (item) => {
const trimmedItem = item.trim();
🤖 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 28 - 29, The bulk lookup
handler in route.ts should validate each element in items before calling trim()
inside lookupPromises. Update the item processing logic to reject non-string
entries early, returning a client error instead of letting item.trim() throw and
bubble into a 500. Use the existing bulk request flow around lookupPromises and
the item handling path to add a per-item type check before trimming and lookup.

Comment on lines 40 to 42
if (trimmedItem.match(/^(https?:\/\/|www\.)/i)) {
apiEndpoint = "/api/lookup/url";
handler = urlLookup;
body = { url: trimmedItem };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize www. URLs before dispatching.

This branch accepts www.example.com, but the URL handler calls new URL(url), so it returns “Invalid URL format.” Prefix https:// before passing the body.

Proposed fix
         if (trimmedItem.match(/^(https?:\/\/|www\.)/i)) {
           handler = urlLookup;
-          body = { url: trimmedItem };
+          body = { url: trimmedItem.match(/^www\./i) ? `https://${trimmedItem}` : trimmedItem };
📝 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
if (trimmedItem.match(/^(https?:\/\/|www\.)/i)) {
apiEndpoint = "/api/lookup/url";
handler = urlLookup;
body = { url: trimmedItem };
if (trimmedItem.match(/^(https?:\/\/|www\.)/i)) {
handler = urlLookup;
body = { url: trimmedItem.match(/^www\./i) ? `https://${trimmedItem}` : trimmedItem };
🤖 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 40 - 42, The bulk lookup URL
branch in the route handler accepts values starting with www. but passes them
directly to urlLookup, which later parses them with new URL and fails. Update
the URL-handling logic in the bulk route to normalize www. inputs by prefixing
https:// before assigning the body, while keeping existing http/https URLs
unchanged.

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.

1 participant