Skip to content

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

Open
aicoder2009 wants to merge 1 commit into
mainfrom
sentinel-fix-ssrf-bulk-lookup-15885741833350580002
Open

🛡️ Sentinel: [CRITICAL] Fix SSRF vulnerability in bulk lookup API#202
aicoder2009 wants to merge 1 commit into
mainfrom
sentinel-fix-ssrf-bulk-lookup-15885741833350580002

Conversation

@aicoder2009

@aicoder2009 aicoder2009 commented Jul 1, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: CRITICAL
💡 Vulnerability: The application was making loopback HTTP requests to itself using fetch(request.nextUrl.origin + '/api/...') within the bulk lookup API endpoint. Using request.nextUrl.origin inside a server route allows an attacker to control the Host header.
🎯 Impact: This created a Server-Side Request Forgery (SSRF) vulnerability. An attacker could spoof the Host header in their request, causing the internal fetch call to be redirected to a malicious external server. This could lead to information disclosure or allow an attacker to probe internal services.
🔧 Fix: I replaced the vulnerable HTTP loopback fetch() calls with direct function invocations of the respective Next.js route handlers (urlPOST, doiPOST, isbnPOST). I constructed synthetic NextRequest objects to pass to these handlers, safely bypassing any network-level routing and host origin dependencies.
Verification: The file src/app/api/lookup/bulk/route.ts now uses direct handler invocations. The test file src/app/api/lookup/bulk/route.test.ts has been updated to mock the imported route handlers instead of global.fetch. Running pnpm test:run validates that all lookups behave correctly.


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

Summary by CodeRabbit

  • Bug Fixes
    • Improved bulk lookup reliability by routing requests through the app’s internal lookup flow instead of making self-referential network calls.
    • Reduced the risk of failed or misrouted lookups when processing URL, DOI, and ISBN items.
    • Strengthened protection against a server-side request abuse issue tied to bulk lookup handling.

Removed dangerous dynamic loopback HTTP fetches that relied on the attacker-controllable `request.nextUrl.origin` inside the `src/app/api/lookup/bulk/route.ts` API endpoint.

Directly imported and invoked the target Next.js `POST` route handlers using synthetic `NextRequest` objects, entirely eliminating the Server-Side Request Forgery (SSRF) vector while preserving functionality. Tests were updated to reflect and verify this safer internal routing.

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 Jul 1, 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 Jul 1, 2026 6:44am

Copilot AI review requested due to automatic review settings July 1, 2026 06:43
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The bulk lookup route is refactored to directly invoke internal URL/DOI/ISBN POST handlers with synthetic NextRequest objects instead of fetching via request.nextUrl.origin, eliminating a loopback fetch pattern. Tests are updated to mock the internal handlers instead of global.fetch. A security documentation entry describes the SSRF risk and fix.

Changes

SSRF fix in bulk lookup handler

Layer / File(s) Summary
Direct handler invocation in bulk route
src/app/api/lookup/bulk/route.ts
Imports internal POST handlers for URL/DOI/ISBN lookups, selects the appropriate handler per item, and invokes it with a synthetic NextRequest targeting http://localhost instead of using fetch with a dynamically built URL.
Tests mocking internal handlers
src/app/api/lookup/bulk/route.test.ts
Replaces global.fetch mocking with vi.mock of the URL/DOI/ISBN sub-route modules; rewrites routing, error-status, and summary/count tests to use mocked urlPOST/doiPOST/isbnPOST responses.
Security log entry
.jules/sentinel.md
Adds a dated entry documenting the SSRF risk from loopback fetch calls using request.nextUrl.origin and the remediation of invoking route handlers directly.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BulkRoute
  participant urlPOST
  participant doiPOST
  participant isbnPOST

  Client->>BulkRoute: POST batch of lookup items
  loop each item
    BulkRoute->>BulkRoute: detect item format (URL/DOI/ISBN)
    alt URL item
      BulkRoute->>urlPOST: synthetic NextRequest with JSON body
      urlPOST-->>BulkRoute: JSON response
    else DOI item
      BulkRoute->>doiPOST: synthetic NextRequest with JSON body
      doiPOST-->>BulkRoute: JSON response
    else ISBN item
      BulkRoute->>isbnPOST: synthetic NextRequest with JSON body
      isbnPOST-->>BulkRoute: JSON response
    end
  end
  BulkRoute-->>Client: aggregated success/failure summary
Loading

Related Issues: None referenced in the provided changes.

Suggested labels: security, refactor, tests

Suggested reviewers: None determinable from the provided information.

🐇 A loopback closed, no more the fetch that strayed,
Now handlers speak in-process, quiet and unafraid,
Mocked tests confirm the routes still find their way,
A sentinel note recalls the risk, now put away.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing a critical SSRF issue in the bulk lookup API.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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-fix-ssrf-bulk-lookup-15885741833350580002

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: lockfile failed supply-chain policy check. Run pnpm install locally to update the lockfile.


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.

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.

Pull request overview

This PR mitigates a critical SSRF risk in the bulk lookup API by removing internal loopback fetch(request.nextUrl.origin + ...) calls and instead directly invoking the relevant Next.js route handlers with synthetic NextRequest objects.

Changes:

  • Replaced internal loopback fetch() calls in the bulk lookup route with direct handler invocations (urlPOST, doiPOST, isbnPOST).
  • Updated bulk lookup tests to mock imported route handlers instead of global.fetch.
  • Added a Sentinel entry documenting the SSRF learning/prevention guidance.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/app/api/lookup/bulk/route.ts Removes Host-header-influenced loopback fetches by calling internal lookup handlers directly.
src/app/api/lookup/bulk/route.test.ts Switches from mocking fetch to mocking the imported route handlers used by the bulk route.
.jules/sentinel.md Documents the SSRF pattern and the preferred mitigation approach for internal route calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1 to +5
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { POST } from './route';
import { NextRequest } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';

global.fetch = vi.fn();
vi.mock('../url/route', () => ({ POST: vi.fn() }));
Comment on lines +36 to 40
let handler: typeof urlPOST;
let body: object;

// Detect input type
if (trimmedItem.match(/^(https?:\/\/|www\.)/i)) {
Comment thread .jules/sentinel.md
**Vulnerability:** The application was using the `marked` library to parse Markdown content into HTML (in `src/app/docs/changelog/page.tsx` and `src/lib/docs.ts`) and subsequently rendering it using `dangerouslySetInnerHTML` without proper sanitization.
**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`.
## 2025-07-01 - Prevent SSRF in Next.js Server Actions

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

63-90: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the synthetic sub-request contract in the routing tests.

These cases only prove that a mock was called. If the bulk route sends the wrong JSON shape or accidentally calls multiple sub-handlers, they still pass because the mocks ignore the request argument. Please assert the selected handler receives the expected { url } / { doi } / { isbn } body and that the other handlers stay untouched. As per coding guidelines, **/{src/lib/citation/**/*.test.ts,src/app/api/**/*.test.ts}: Write tests for citation formatting and API route logic.

Example assertion pattern
     expect(data.results[0].success).toBe(true);
-    expect(urlPOST).toHaveBeenCalled();
+    expect(urlPOST).toHaveBeenCalledTimes(1);
+    expect(doiPOST).not.toHaveBeenCalled();
+    expect(isbnPOST).not.toHaveBeenCalled();
+    const [requestArg] = (urlPOST as ReturnType<typeof vi.fn>).mock.calls[0];
+    expect(await requestArg.json()).toEqual({ url: 'https://example.com' });
🤖 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 63 - 90, The bulk routing
tests for POST only verify that a handler was called, so they can miss incorrect
sub-request payloads or extra handler invocations. Update the POST tests in
route.test.ts to assert the selected mock handler receives the expected request
body shape for each case ({ url }, { doi }, { isbn }) using the request argument
passed into urlPOST, doiPOST, and isbnPOST. Also verify the other handler mocks
remain untouched for each scenario so the routing contract is fully covered.

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:
- Around line 9-12: Retitle this note to refer to route handlers instead of
Server Actions, since the issue is in the bulk lookup route handler rather than
an action. Update the heading in the sentinel entry and keep the rest of the
guidance aligned with the route handler pattern described by the bulk lookup
handler symbols such as the lookup route’s POST flow, so readers are pointed at
the correct Next.js surface.

In `@src/app/api/lookup/bulk/route.ts`:
- Around line 29-32: The bulk lookup mapper in the route handler should validate
each entry before calling trim() so a non-string item does not throw outside the
per-item try/catch. Update the lookupPromises logic to check the item type
first, return a validation-style result for invalid entries, and only call
trim() on confirmed strings inside the same item-level handling used by the bulk
route.

---

Nitpick comments:
In `@src/app/api/lookup/bulk/route.test.ts`:
- Around line 63-90: The bulk routing tests for POST only verify that a handler
was called, so they can miss incorrect sub-request payloads or extra handler
invocations. Update the POST tests in route.test.ts to assert the selected mock
handler receives the expected request body shape for each case ({ url }, { doi
}, { isbn }) using the request argument passed into urlPOST, doiPOST, and
isbnPOST. Also verify the other handler mocks remain untouched for each scenario
so the routing contract is fully covered.
🪄 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: e08d1a16-b759-4464-aeb3-cabbb90dc988

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 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
Comment on lines +9 to +12
## 2025-07-01 - Prevent SSRF in Next.js Server Actions
**Vulnerability:** The application was previously making loopback HTTP requests to itself using `fetch(request.nextUrl.origin + '/api/...')` in the bulk lookup API. Using `request.nextUrl.origin` inside a server route allows an attacker to control the `Host` header, leading to Server-Side Request Forgery (SSRF) and redirecting internal server traffic to malicious external domains.
**Learning:** Next.js route handlers are standard async functions. We do not need to use HTTP `fetch()` to call another internal API route from within the server. We can import the route function directly and invoke it.
**Prevention:** Always directly import and invoke internal Next.js route handlers (e.g. `import { POST } from '../other/route'`) using a synthetic `NextRequest` object rather than relying on `fetch` with dynamically derived hostnames.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Retitle this entry to route handlers, not Server Actions.

Line 9 points readers at the wrong surface: the issue described in this PR came from src/app/api/lookup/bulk/route.ts, which is a route handler, not a Server Action.

Suggested fix
-## 2025-07-01 - Prevent SSRF in Next.js Server Actions
+## 2025-07-01 - Prevent SSRF in Next.js Route Handlers
📝 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
## 2025-07-01 - Prevent SSRF in Next.js Server Actions
**Vulnerability:** The application was previously making loopback HTTP requests to itself using `fetch(request.nextUrl.origin + '/api/...')` in the bulk lookup API. Using `request.nextUrl.origin` inside a server route allows an attacker to control the `Host` header, leading to Server-Side Request Forgery (SSRF) and redirecting internal server traffic to malicious external domains.
**Learning:** Next.js route handlers are standard async functions. We do not need to use HTTP `fetch()` to call another internal API route from within the server. We can import the route function directly and invoke it.
**Prevention:** Always directly import and invoke internal Next.js route handlers (e.g. `import { POST } from '../other/route'`) using a synthetic `NextRequest` object rather than relying on `fetch` with dynamically derived hostnames.
## 2025-07-01 - Prevent SSRF in Next.js Route Handlers
**Vulnerability:** The application was previously making loopback HTTP requests to itself using `fetch(request.nextUrl.origin + '/api/...')` in the bulk lookup API. Using `request.nextUrl.origin` inside a server route allows an attacker to control the `Host` header, leading to Server-Side Request Forgery (SSRF) and redirecting internal server traffic to malicious external domains.
**Learning:** Next.js route handlers are standard async functions. We do not need to use HTTP `fetch()` to call another internal API route from within the server. We can import the route function directly and invoke it.
**Prevention:** Always directly import and invoke internal Next.js route handlers (e.g. `import { POST } from '../other/route'`) using a synthetic `NextRequest` object rather than relying on `fetch` with dynamically derived hostnames.
🤖 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 around lines 9 - 12, Retitle this note to refer to route
handlers instead of Server Actions, since the issue is in the bulk lookup route
handler rather than an action. Update the heading in the sentinel entry and keep
the rest of the guidance aligned with the route handler pattern described by the
bulk lookup handler symbols such as the lookup route’s POST flow, so readers are
pointed at the correct Next.js surface.

Comment on lines 29 to 32
const lookupPromises = items.map(async (item) => {
const trimmedItem = item.trim();
if (!trimmedItem) {
return { input: item, success: false, error: "Empty input" };

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 | 🟠 Major | ⚡ Quick win

Validate each batch item before calling .trim().

Line 30 assumes every array element is a string, and that happens before the per-item try. A payload like { items: [null, '10.1000/xyz'] } will reject the mapper, make Promise.all(...) fail, and turn the whole batch into a 500 instead of a validation error for that entry.

Suggested fix
-    const lookupPromises = items.map(async (item) => {
-      const trimmedItem = item.trim();
+    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();
📝 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 (!trimmedItem) {
return { input: item, success: false, error: "Empty input" };
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 29 - 32, The bulk lookup
mapper in the route handler should validate each entry before calling trim() so
a non-string item does not throw outside the per-item try/catch. Update the
lookupPromises logic to check the item type first, return a validation-style
result for invalid entries, and only call trim() on confirmed strings inside the
same item-level handling used by the bulk route.

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