Repository navigation
test(security): add SSRF validation edge-case coverage #898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
96e23b6
3607626
f642f35
00b91d2
e1704e3
a43f231
548ec92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -176,3 +176,117 @@ describe("validateEndpointUrl", () => { | |||||||||||||||||||||||
| await expect(validateEndpointUrl(url)).resolves.toBe(url); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // ── Edge-case coverage ──────────────────────────────────────────── | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| describe("isPrivateIp – CIDR boundary precision", () => { | ||||||||||||||||||||||||
| it.each([ | ||||||||||||||||||||||||
| ["172.15.255.255", false], // just below 172.16.0.0/12 | ||||||||||||||||||||||||
| ["172.16.0.0", true], // first address in 172.16.0.0/12 | ||||||||||||||||||||||||
| ["172.31.255.255", true], // last address in 172.16.0.0/12 | ||||||||||||||||||||||||
| ["172.32.0.0", false], // just above 172.16.0.0/12 | ||||||||||||||||||||||||
| ["169.253.255.255", false], // just below 169.254.0.0/16 | ||||||||||||||||||||||||
| ["169.254.0.0", true], // first address in 169.254.0.0/16 | ||||||||||||||||||||||||
| ["169.255.0.0", false], // just above 169.254.0.0/16 | ||||||||||||||||||||||||
| ["10.0.0.0", true], // first address in 10.0.0.0/8 | ||||||||||||||||||||||||
| ["11.0.0.0", false], // just above 10.0.0.0/8 | ||||||||||||||||||||||||
| ["126.255.255.255", false], // just below 127.0.0.0/8 | ||||||||||||||||||||||||
| ["128.0.0.0", false], // just above 127.0.0.0/8 | ||||||||||||||||||||||||
| ["192.167.255.255", false], // just below 192.168.0.0/16 | ||||||||||||||||||||||||
| ["192.169.0.0", false], // just above 192.168.0.0/16 | ||||||||||||||||||||||||
| ])("boundary %s → private=%s", (ip, expected) => { | ||||||||||||||||||||||||
| expect(isPrivateIp(ip)).toBe(expected); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| describe("isPrivateIp – IPv6 edge cases", () => { | ||||||||||||||||||||||||
| it.each([ | ||||||||||||||||||||||||
| // link-local and multicast are NOT in PRIVATE_NETWORKS — verify they're treated as public | ||||||||||||||||||||||||
| ["fe80::1", false], | ||||||||||||||||||||||||
| ["ff02::1", false], | ||||||||||||||||||||||||
| // zero address | ||||||||||||||||||||||||
| ["::0", false], | ||||||||||||||||||||||||
| // fd00::/8 boundaries | ||||||||||||||||||||||||
| ["fcff::1", false], // fc00::/8 is NOT protected, only fd00::/8 | ||||||||||||||||||||||||
| ["fd00::0", true], // first address in fd00::/8 | ||||||||||||||||||||||||
| ["fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true], // last address in fd00::/8 | ||||||||||||||||||||||||
| ["fe00::1", false], // just above fd00::/8 | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| // fd00::/8 boundaries | |
| ["fcff::1", false], // fc00::/8 is NOT protected, only fd00::/8 | |
| ["fd00::0", true], // first address in fd00::/8 | |
| ["fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true], // last address in fd00::/8 | |
| ["fe00::1", false], // just above fd00::/8 | |
| // fc00::/7 (RFC 4193 Unique Local Addresses) boundaries | |
| ["fbff::1", false], // just below fc00::/7 | |
| ["fcff::1", true], // within fc00::/7 ULA range | |
| ["fd00::0", true], // first address in fd00::/8 (within fc00::/7) | |
| ["fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true], // last address in fc00::/7 ULA range | |
| ["fe00::1", false], // just above fc00::/7 |
Copilot
AI
Mar 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test name is misleading: https://user:pass@api.example.com/v1 is userinfo/basic auth in the URL (not “in hostname”). Renaming the test description to reflect userinfo will make it clearer what behavior is being validated.
| it("allows URL with basic auth in hostname", async () => { | |
| it("allows URL with userinfo/basic auth", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IPv6 expectations here treat link-local (fe80::/10), multicast (ff00::/8), and the unspecified address (::/128) as “public”. For SSRF protection these ranges are non-globally-routable and are commonly treated as internal/reserved, so allowing them can enable access to local interfaces/services. Consider updating
isPrivateIp/PRIVATE_NETWORKSto block these reserved ranges and adjust these tests to expect rejection (private=true) accordingly.