Skip to content

🛡️ Sentinel: [HIGH] Fix SSRF in Bulk Lookup API#136

Open
aicoder2009 wants to merge 1 commit into
mainfrom
sentinel/ssrf-bulk-api-fix-16062685776947422308
Open

🛡️ Sentinel: [HIGH] Fix SSRF in Bulk Lookup API#136
aicoder2009 wants to merge 1 commit into
mainfrom
sentinel/ssrf-bulk-api-fix-16062685776947422308

Conversation

@aicoder2009
Copy link
Copy Markdown
Owner

@aicoder2009 aicoder2009 commented May 30, 2026

Severity: HIGH
Vulnerability: Server-Side Request Forgery (SSRF) via dynamically derived loopback URLs
Impact: An attacker could spoof the HTTP Host header to redirect the internal fetch call to malicious external endpoints or private internal IPs.
Fix: Updated the API endpoint to directly import and invoke the required POST route handlers using a simulated NextRequest object instead of routing them over the network via fetch.
Verification: Verified against testing suite (run pnpm test) and unit tests now mock the internal endpoints appropriately.


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

Summary by CodeRabbit

  • Bug Fixes

    • Fixed a security vulnerability in the bulk lookup API that could allow request redirection to unintended targets.
  • Documentation

    • Added security documentation detailing the vulnerability and mitigation approach.

Review Change Stack

Removed loopback fetch requests in `src/app/api/lookup/bulk/route.ts` which relied on the user-controlled `request.nextUrl.origin` and HTTP Host header. Replaced with direct invocation of Next.js Route Handlers using a synthesized `NextRequest` to eliminate the SSRF vector and improve API performance. Also added test mocks.

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 May 30, 2026 06:44
@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

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

Project Deployment Actions Updated (UTC)
opencitation Ready Ready Preview, Comment May 30, 2026 6:45am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

The PR refactors the bulk lookup API to prevent SSRF by eliminating loopback fetch() calls. Instead, handlers are imported and invoked directly with synthesized NextRequest objects, documented as a security mitigation and validated through updated tests that mock route modules.

Changes

SSRF Prevention via Direct Handler Invocation

Layer / File(s) Summary
SSRF Vulnerability Documentation
.jules/sentinel.md
Security sentinel entry documents an SSRF vulnerability caused by host-header-derived origin in loopback fetch calls and records the mitigation of direct handler invocation instead of network requests.
Direct Handler Invocation Implementation
src/app/api/lookup/bulk/route.ts
Bulk route imports URL/DOI/ISBN handlers, refactors per-item routing to select the appropriate handler and construct a NextRequest with a route URL, then invokes the handler directly instead of making fetch calls to internal endpoints.
Test Suite Update
src/app/api/lookup/bulk/route.test.ts
Test file mocks the lookup route modules with vi.mock to provide deterministic POST responses, updates routing tests to assert returned result data directly, removes prior fetch call URL assertions, and adjusts batch inputs to match the mocked behaviors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A loopback avoided, a fetch turned around,
Direct invocation—no network sound!
Handlers called locally, SSRF gone,
Tests remocked cleanly to carry us on.

🚥 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 includes emoji and formatting that add noise, but clearly identifies the main change as fixing an SSRF vulnerability in the Bulk Lookup API, which aligns with the core objective.
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 sentinel/ssrf-bulk-api-fix-16062685776947422308

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Remediates an SSRF risk in the bulk lookup API by removing loopback network calls that could be influenced by a spoofed Host header, and instead dispatching to the internal lookup route handlers directly.

Changes:

  • Replaced internal loopback fetch() calls in bulk lookup with direct invocation of /api/lookup/{url|doi|isbn} route handlers via synthesized NextRequest.
  • Updated bulk lookup tests to mock the imported internal route modules instead of mocking fetch().
  • Added a Sentinel entry documenting the SSRF vulnerability pattern and prevention guidance.

Reviewed changes

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

File Description
src/app/api/lookup/bulk/route.ts Eliminates loopback network dispatch by directly calling internal lookup route handlers.
src/app/api/lookup/bulk/route.test.ts Switches tests from fetch mocking to module-level route handler mocks for the new dispatch approach.
.jules/sentinel.md Documents the SSRF finding and the preferred mitigation pattern for internal route-to-route calls.

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

Comment on lines +61 to +68
// Invoke the route handler directly to prevent SSRF
const mockRequest = new NextRequest(routeUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});

const response = await routeHandler(mockRequest);
Comment on lines +42 to +52
routeHandler = lookupUrlPost;
body = { url: trimmedItem };
routeUrl = "http://localhost/api/lookup/url";
} else if (trimmedItem.match(/^10\.\d{4,}/)) {
apiEndpoint = "/api/lookup/doi";
routeHandler = lookupDoiPost;
body = { doi: trimmedItem };
routeUrl = "http://localhost/api/lookup/doi";
} else if (trimmedItem.match(/^(97[89])?\d{9}[\dXx]$/)) {
apiEndpoint = "/api/lookup/isbn";
routeHandler = lookupIsbnPost;
body = { isbn: trimmedItem };
routeUrl = "http://localhost/api/lookup/isbn";
Comment on lines 4 to 6

global.fetch = vi.fn();

Comment on lines +7 to +15
vi.mock('@/app/api/lookup/url/route', () => ({
POST: vi.fn(async (req) => {
const data = await req.json();
if (data.url === 'https://example.com' || data.url === 'https://url-test.com') {
return new Response(JSON.stringify({ data: { title: data.url === 'https://example.com' ? 'Example Page' : 'URL result' } }), { status: 200 });
}
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
})
}));
Comment on lines +17 to +25
vi.mock('@/app/api/lookup/doi/route', () => ({
POST: vi.fn(async (req) => {
const data = await req.json();
if (data.doi === '10.1000/xyz123' || data.doi === '10.1234/test') {
return new Response(JSON.stringify({ data: { title: 'DOI result' } }), { status: 200 });
}
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
})
}));
Comment on lines +27 to +35
vi.mock('@/app/api/lookup/isbn/route', () => ({
POST: vi.fn(async (req) => {
const data = await req.json();
if (data.isbn === '9780316769174') {
return new Response(JSON.stringify({ data: { title: 'ISBN result' } }), { status: 200 });
}
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 });
})
}));
Comment on lines 86 to 91
it('routes URLs to /api/lookup/url', async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: { title: 'Example Page' } }),
});
const response = await POST(makeRequest({ items: ['https://example.com'] }));
const data = await response.json();
expect(data.results[0].success).toBe(true);
expect(data.results[0].data.title).toBe('Example Page');
const [url] = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string];
expect(url).toContain('/api/lookup/url');
});
Comment thread .jules/sentinel.md
Comment on lines +9 to +12
## 2025-02-28 - [SSRF via loopback fetch using user-controlled Host header]
**Vulnerability:** The bulk lookup API (`src/app/api/lookup/bulk/route.ts`) performed loopback HTTP requests to other internal endpoints using `fetch()` and dynamically derived the host via `request.nextUrl.origin`. Because `request.nextUrl.origin` relies on the HTTP `Host` header, an attacker could spoof it, redirecting the internal `fetch()` to an arbitrary external server or internal IP (a Server-Side Request Forgery vulnerability).
**Learning:** Next.js Route Handlers (API endpoints) run on the server side and should not invoke each other via network `fetch()` unless absolutely necessary. When doing so with a user-provided or dynamically derived URL based on request headers, it immediately creates an SSRF risk.
**Prevention:** Rather than using `fetch()` to call internal API endpoints, import their exported `POST`, `GET`, etc., functions directly and invoke them with a synthesized or properly formatted `NextRequest`. This prevents DNS/network routing vulnerabilities and is also more performant.
Copy link
Copy Markdown

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

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

5-5: ⚡ Quick win

Remove unused global.fetch mock.

Since the implementation no longer uses fetch() (replaced with direct handler invocation), this mock is dead code.

🧹 Proposed cleanup
-global.fetch = vi.fn();
-
 vi.mock('`@/app/api/lookup/url/route`', () => ({
🤖 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` at line 5, Remove the now-unused
global.fetch mock in the test: delete the line that sets global.fetch = vi.fn()
from route.test.ts (the top-level test setup), since tests call the handler
directly and no code under test uses fetch; this removes dead test setup and
avoids misleading mocks.
🤖 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.

Nitpick comments:
In `@src/app/api/lookup/bulk/route.test.ts`:
- Line 5: Remove the now-unused global.fetch mock in the test: delete the line
that sets global.fetch = vi.fn() from route.test.ts (the top-level test setup),
since tests call the handler directly and no code under test uses fetch; this
removes dead test setup and avoids misleading mocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 430241ad-ff83-4b55-bce2-5fd5cc418b69

📥 Commits

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

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

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