fix(security): resolve IPv6-mapped IPv4 bypass in SSRF protection#2176
Conversation
|
@VIDYANKSHINI is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel. A member of the Team first needs to authorize it. |
GSSoC Label Checklist 🏷️@Priyanshu-byte-coder — please apply the appropriate labels before merging: Difficulty (pick one):
Quality (optional):
Validation (required to score):
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR strengthens SSRF defenses by improving IP parsing/private-network detection (including IPv6 cases) and expanding isSafeUrl DNS resolution to cover both A and AAAA records, with new tests added around these behaviors.
Changes:
- Harden IPv4 parsing and private-IP detection (incl. IPv6-mapped IPv4 handling and invalid format blocking).
- Update
isSafeUrlto resolve both A and AAAA DNS records and reject more localhost bypass patterns. - Add Vitest coverage for
isSafeUrlusing mocked DNS resolution.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| test/ssrf-protection.test.ts | Adds unit tests for isSafeUrl, including protocol filtering and private/public IP outcomes via DNS mocks. |
| src/lib/ssrf-protection.ts | Enhances IP parsing/private detection and extends isSafeUrl to check both IPv4 and IPv6 DNS records. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const parts = ip.split("."); | ||
| if (parts.length !== 4) return NaN; | ||
| const numParts = parts.map(Number); | ||
| if (numParts.some(isNaN)) return NaN; | ||
| return ((numParts[0] << 24) | (numParts[1] << 16) | (numParts[2] << 8) | numParts[3]) >>> 0; |
| // Allow basic bypassing localhost blocks if they try to bypass resolution completely | ||
| if (hostname === "localhost" || hostname === "0.0.0.0" || hostname === "[::1]") { | ||
| return false; | ||
| } |
| const addresses: string[] = []; | ||
|
|
||
| try { | ||
| const aRecords = await resolve(hostname, "A"); | ||
| if (Array.isArray(aRecords)) addresses.push(...(aRecords as string[])); | ||
| } catch {} | ||
|
|
||
| try { | ||
| const aaaaRecords = await resolve(hostname, "AAAA"); | ||
| if (Array.isArray(aaaaRecords)) addresses.push(...(aaaaRecords as string[])); | ||
| } catch {} | ||
|
|
||
| if (addresses.length === 0) { | ||
| return false; | ||
| } |
| // Allow basic bypassing localhost blocks if they try to bypass resolution completely | ||
| if (hostname === "localhost" || hostname === "0.0.0.0" || hostname === "[::1]") { |
| import dns from "dns"; | ||
|
|
||
| vi.mock("dns", () => ({ | ||
| default: { | ||
| resolve: vi.fn(), | ||
| }, | ||
| })); |
| mockResolve.mockImplementation((hostname: string, rrtype: string, cb: Function) => { | ||
| if (rrtype === "A") cb(null, ["8.8.8.8"]); | ||
| else cb(new Error("ENOTFOUND")); | ||
| }); |
| import { validateUrlBasic } from "../src/lib/ssrf-protection"; | ||
| import { describe, it, expect } from "vitest"; | ||
| import { validateUrlBasic, isSafeUrl } from "../src/lib/ssrf-protection"; | ||
| import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; |
77b1387
into
Priyanshu-byte-coder:main
|
🎉 Merged! Thanks for contributing to DevTrack. If the project has been useful to you, a ⭐ star on the repo is the easiest way to support it — it helps DevTrack get discovered by more developers. Keep an eye on open issues for your next contribution! |
Summary
Fixed a critical SSRF vulnerability bypass involving IPv6-mapped IPv4 addresses and AAAA-only hosts, and patched an underlying 32-bit signed integer overflow bug in the IP evaluation logic.
Closes #2151
Type of Change
Changes Made
ipToNumberto return a 32-bit unsigned integer (via>>> 0). Previously, calculating192 << 24resulted in a negative number, allowing internal IPs like192.168.x.xto silently bypass the bounds check.::ffff:192.168.1.1) and validate it normally.::1,::,fe80::,fc00::,fd00::).isSafeUrlto resolve bothAandAAAArecords. If any resolved IP points to a private/internal network, the request is blocked.dns.resolveto verify behavior for IPv6, mapped IPv4, and public/private resolutions.How to Test
npm run testlocally and verify that thessrf-protection.test.tssuite passes, including all the newisSafeUrltest cases.AAAArecord like::ffff:169.254.169.254returnsfalse.Screenshots (if UI change)
N/A
Checklist
npm run lintpasses locallynpm run type-check)