Skip to content

🛡️ Sentinel: [HIGH] Fix Host-header SSRF in bulk lookup API#155

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

🛡️ Sentinel: [HIGH] Fix Host-header SSRF in bulk lookup API#155
aicoder2009 wants to merge 1 commit into
mainfrom
fix-ssrf-bulk-lookup-10779170663556295043

Conversation

@aicoder2009

@aicoder2009 aicoder2009 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: HIGH
💡 Vulnerability: The /api/lookup/bulk/route.ts endpoint performed internal API loopback HTTP fetch requests constructed using a dynamically derived hostname (request.nextUrl.origin). This relies on the HTTP Host header, which is user-controllable. An attacker could manipulate this header to route the internal lookup requests to an arbitrary internal IP or domain (Host-header Server-Side Request Forgery).
🎯 Impact: An attacker could potentially bypass SSRF protections, access restricted internal network endpoints, or perform cache poisoning depending on the environment architecture.
🔧 Fix: Refactored the endpoint to avoid loopback fetch calls entirely. Internal route handlers (urlPost, doiPost, isbnPost) are now directly imported and invoked using a synthetic NextRequest object (new NextRequest(new URL('http://localhost'), ...)). This avoids DNS/network boundaries, effectively neutralizing the SSRF vector while simultaneously improving latency.
Verification: Unit tests for /api/lookup/bulk/route.test.ts have been updated to mock the local route files instead of global.fetch and pass successfully (pnpm test).


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

Summary by CodeRabbit

  • Bug Fixes

    • Resolved a host-header SSRF vulnerability in the bulk lookup API.
  • Tests

    • Updated bulk lookup test suite to verify correct routing behavior.
  • Documentation

    • Documented security finding related to bulk lookup API request handling.

Removed loopback fetch calls utilizing user-controllable Host headers in `/api/lookup/bulk/route.ts` which presented a Host-header SSRF vulnerability.

Mitigated this by importing and directly invoking internal route handlers (`../url/route`, `../doi/route`, and `../isbn/route`) utilizing a synthetic NextRequest, increasing security and lowering request latency.

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 9, 2026 06:38
@vercel

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

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 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR eliminates a Host-header SSRF vulnerability in the bulk lookup API by replacing loopback HTTP fetch calls with direct in-process handler invocation. The security issue, mitigation approach, implementation refactoring, and test updates are coordinated to remove the fetch-based origin dependency while maintaining identical external behavior.

Changes

SSRF Vulnerability Mitigation

Layer / File(s) Summary
Security documentation
.jules/sentinel.md
Documents Host-header SSRF vulnerability in loopback fetch and records mitigation: invoke internal handlers directly instead of via HTTP.
Handler invocation refactoring
src/app/api/lookup/bulk/route.ts
Imports POST handlers from URL/DOI/ISBN sub-routes and invokes them directly via synthetic NextRequest; detects item type via regex, routes to appropriate handler, and awaits response without HTTP fetch.
Test suite updates
src/app/api/lookup/bulk/route.test.ts
Mocks internal handler functions instead of global.fetch; rewrites routing and batch tests to verify correct handler invocation and control outcomes via mocked handler responses.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A loopback fetch, oh what a plight,
Host headers bent, security not tight,
But handlers called direct—a safer way,
The SSRF shadow now held at bay! 🔒

🚥 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 a Host-header SSRF vulnerability in the bulk lookup API by replacing loopback fetch with direct handler invocation.
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-10779170663556295043

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

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

69-77: ⚡ Quick win

Assert dispatched payloads, not only handler invocation.

These tests currently prove routing path selection, but not JSON contract correctness ({ url }, { doi }, { isbn }). Add body assertions on the mocked NextRequest argument to catch silent contract regressions.

Example assertion pattern
   it('routes URLs to /api/lookup/url', async () => {
     (urlPost as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
       NextResponse.json({ data: { title: 'Example Page' } }, { status: 200 })
     );
     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');
     expect(urlPost).toHaveBeenCalled();
+    const req = (urlPost as ReturnType<typeof vi.fn>).mock.calls[0][0] as NextRequest;
+    await expect(req.json()).resolves.toEqual({ url: 'https://example.com' });
   });

Also applies to: 80-87, 90-97

🤖 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 69 - 77, Update the tests
to assert the dispatched request payloads (not just that handlers were called):
for the URL case, grab the first argument passed to the mocked urlPost (e.g.,
const req = (urlPost as vi.Mock).mock.calls[0][0]), await req.json() and assert
it equals { url: 'https://example.com' }; do the same for the DOI and ISBN tests
by inspecting doiPost and isbnPost calls respectively and asserting their JSON
bodies equal { doi: '...' } and { isbn: '...' } using the existing POST and
makeRequest helpers so any contract regressions are caught.
🤖 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 28-31: The map over items (lookupPromises) calls item.trim()
without guarding non-string values, causing a 500 on bad inputs; update the
items.map callback (the lookupPromises function) to first check typeof item ===
"string" (or otherwise validate the type) and immediately return a per-item
failure object like { input: item, success: false, error: "Invalid input type" }
for non-strings, then continue with const trimmedItem = item.trim() for valid
strings so each bad entry yields a per-item validation error instead of crashing
the whole request.

---

Nitpick comments:
In `@src/app/api/lookup/bulk/route.test.ts`:
- Around line 69-77: Update the tests to assert the dispatched request payloads
(not just that handlers were called): for the URL case, grab the first argument
passed to the mocked urlPost (e.g., const req = (urlPost as
vi.Mock).mock.calls[0][0]), await req.json() and assert it equals { url:
'https://example.com' }; do the same for the DOI and ISBN tests by inspecting
doiPost and isbnPost calls respectively and asserting their JSON bodies equal {
doi: '...' } and { isbn: '...' } using the existing POST and makeRequest helpers
so any contract regressions are caught.
🪄 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: 92ef7d38-6fe4-4316-b0de-125a5bf687e5

📥 Commits

Reviewing files that changed from the base of the PR and between 2402643 and 9606f2e.

📒 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 31
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle non-string items entries before trimming.

Line 29 calls .trim() before any guard; if a client sends a non-string element, the whole request falls into the outer catch and returns 500 instead of a per-item validation failure.

Proposed 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();
       if (!trimmedItem) {
         return { input: item, success: false, error: "Empty input" };
       }
📝 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 28 - 31, The map over items
(lookupPromises) calls item.trim() without guarding non-string values, causing a
500 on bad inputs; update the items.map callback (the lookupPromises function)
to first check typeof item === "string" (or otherwise validate the type) and
immediately return a per-item failure object like { input: item, success: false,
error: "Invalid input type" } for non-strings, then continue with const
trimmedItem = item.trim() for valid strings so each bad entry yields a per-item
validation error instead of crashing the whole request.

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