Skip to content

Add ssrf guard to storage URL fetch fallback#3896

Open
pepeladeira wants to merge 2 commits into
mainfrom
ssrf-guard-storage-url-fetch
Open

Add ssrf guard to storage URL fetch fallback#3896
pepeladeira wants to merge 2 commits into
mainfrom
ssrf-guard-storage-url-fetch

Conversation

@pepeladeira
Copy link
Copy Markdown
Collaborator

@pepeladeira pepeladeira commented May 12, 2026

Summary by CodeRabbit

  • Security
    • Added stricter URL safety checks to block requests to private or internal IP addresses when processing remote images.
  • Bug Fixes
    • Enforced stricter fetch behavior for remote image retrieval to reduce unexpected network redirects and failures.

Review Change Stack

Review Change Stack

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 12, 2026

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview May 12, 2026 10:40pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

This PR adds SSRF protection to the URL-to-Blob utility by validating resolved hostnames do not map to private, loopback, link-local, or otherwise internal IPv4/IPv6 addresses before performing fetches; fetching uses fetch(url, { redirect: "error" }) in both code paths.

Changes

SSRF Prevention for URL-to-Blob Conversion

Layer / File(s) Summary
IP safety validation helper
apps/web/lib/storage.ts
Imports dns.promises and isIP, and implements assertSafeUrl() to resolve hostnames to IP addresses and reject private/loopback/link-local/unspecified IPv4 and IPv6 ranges.
URL-to-Blob safety integration
apps/web/lib/storage.ts
Integrates assertSafeUrl() before fetching in both the resize/options and no-resize paths of urlToBlob(), removing the proxy-based wsrv.nl fetch and using fetch(url, { redirect: "error" }) after safety validation.
sequenceDiagram
  participant Client
  participant urlToBlob
  participant assertSafeUrl
  participant dns_lookup as dns.promises.lookup
  participant FetchAPI as fetch
  Client->>urlToBlob: request URL -> Blob
  urlToBlob->>assertSafeUrl: assertSafeUrl(url)
  assertSafeUrl->>dns_lookup: lookup(hostname) (if needed)
  dns_lookup-->>assertSafeUrl: resolved IP
  assertSafeUrl-->>urlToBlob: OK / throw on private/internal
  urlToBlob->>FetchAPI: fetch(url, { redirect: "error" }) (if OK)
  FetchAPI-->>urlToBlob: response Blob
  urlToBlob-->>Client: return Blob or throw error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I sniff the bytes and hare a-bound,
I turn away the private ground,
No loopback, link-local, nor RFC-1918,
Only safe URLs hop through my trick. 🥕

🚥 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 accurately reflects the main change: adding SSRF (Server-Side Request Forgery) protection to the storage URL fetch functionality.
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.

✏️ 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 ssrf-guard-storage-url-fetch

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 and usage tips.

Copy link
Copy Markdown
Contributor

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

Caution

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

⚠️ Outside diff range comments (1)
apps/web/lib/storage.ts (1)

218-267: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

The SSRF guard is bypassable via HTTP redirects and DNS rebinding because fetch() performs independent resolution and follows redirects.

A public URL can pass assertSafeUrl() and then be redirected (via 3xx) to 169.254.169.254 or RFC1918 space, since Node.js fetch follows HTTP redirects by default. Additionally, a prior dns.lookup() does not constrain the address that fetch() ultimately connects to—fetch() performs its own independent DNS resolution at connection time, allowing an attacker to exploit DNS rebinding or timing-based resolution changes. The security check must enforce policy at the actual connection/dispatch layer using a custom DNS resolver or connection interceptor, not as a one-off preflight lookup.

🤖 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 `@apps/web/lib/storage.ts` around lines 218 - 267, The current SSRF guard in
assertSafeUrl() is bypassable because fetch() does its own DNS resolution and
follows redirects; modify urlToBlob() to (1) perform fetches with manual
redirect handling (no automatic redirects) and validate every Location
header/redirect target by calling assertSafeUrl() before following it, and (2)
supply fetchWithTimeout (or fetch) with a custom HTTP(S) Agent that implements a
restrictive lookup/resolver (or rejects non-whitelisted/resolved private
addresses) so the actual TCP connection uses your allowed-IP policy; update
references in urlToBlob(), fetchWithTimeout, and assertSafeUrl() to integrate
the agent and redirect validation logic.
🤖 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.

Outside diff comments:
In `@apps/web/lib/storage.ts`:
- Around line 218-267: The current SSRF guard in assertSafeUrl() is bypassable
because fetch() does its own DNS resolution and follows redirects; modify
urlToBlob() to (1) perform fetches with manual redirect handling (no automatic
redirects) and validate every Location header/redirect target by calling
assertSafeUrl() before following it, and (2) supply fetchWithTimeout (or fetch)
with a custom HTTP(S) Agent that implements a restrictive lookup/resolver (or
rejects non-whitelisted/resolved private addresses) so the actual TCP connection
uses your allowed-IP policy; update references in urlToBlob(), fetchWithTimeout,
and assertSafeUrl() to integrate the agent and redirect validation logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2755bbdf-5450-462f-b205-a4bd92b57d37

📥 Commits

Reviewing files that changed from the base of the PR and between 8a42da7 and 5da7e3d.

📒 Files selected for processing (1)
  • apps/web/lib/storage.ts

Comment thread apps/web/lib/storage.ts Dismissed
Comment thread apps/web/lib/storage.ts Dismissed
Copy link
Copy Markdown
Contributor

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

Actionable comments posted: 1

🤖 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 `@apps/web/lib/storage.ts`:
- Around line 225-226: The DNS validation currently discards the validated IP
(address) and then calls fetch(url) with the original hostname, allowing a
TOCTOU DNS rebind; fix by pinning the validated IP to the outbound connection:
either construct an undici Agent/Dispatcher with a custom lookup that returns
the previously-validated address and pass it to fetch (via the dispatcher
option) so the connection uses the validated IP, or rewrite the fetch target to
use the validated IP (replace hostname in the URL) and set the original hostname
in the Host header so TLS/virtual-hosting still works; apply this change where
hostname and address are resolved and where fetch(url) is invoked so the same
validated address is used for the actual connection.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e77ecf75-2409-478e-8bde-fd18f9da09d1

📥 Commits

Reviewing files that changed from the base of the PR and between 5da7e3d and 912f3a2.

📒 Files selected for processing (1)
  • apps/web/lib/storage.ts

Comment thread apps/web/lib/storage.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