mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
* fix: block private IPv6 URLs
* fix: cover full fe80::/10 link-local range and :: unspecified
- Replace startsWith("fe80:") with a check covering the full fe80::/10
range (fe80 through febf) per RFC 4291.
- Add :: (unspecified) to the localhost block.
- Drop the dead 0:0:0:0:0:0:0:1 branch (URL parser normalizes it to ::1).
- Add tests for fe9f::1, febf::1, and ::.
---------
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
/**
|
|
* SSRF (Server-Side Request Forgery) protection utilities
|
|
*/
|
|
|
|
/**
|
|
* Check if URL points to private/internal network
|
|
* Blocks: localhost, private IPs, link-local, AWS metadata service
|
|
*/
|
|
export function isPrivateUrl(urlString: string): boolean {
|
|
try {
|
|
const url = new URL(urlString)
|
|
// Strip a trailing dot so FQDN forms like "localhost." (which still
|
|
// resolve to 127.0.0.1) cannot bypass the equality checks below.
|
|
const hostname = url.hostname
|
|
.toLowerCase()
|
|
.replace(/^\[|\]$/g, "")
|
|
.replace(/\.$/, "")
|
|
|
|
// Block localhost
|
|
if (
|
|
hostname === "localhost" ||
|
|
hostname === "127.0.0.1" ||
|
|
hostname === "::1" ||
|
|
hostname === "::"
|
|
) {
|
|
return true
|
|
}
|
|
|
|
// Block IPv6 unique-local (fc00::/7), link-local (fe80::/10),
|
|
// and IPv4-mapped (::ffff:0:0/96) hosts.
|
|
if (hostname.includes(":")) {
|
|
if (
|
|
hostname.startsWith("fc") ||
|
|
hostname.startsWith("fd") ||
|
|
hostname.startsWith("::ffff:")
|
|
) {
|
|
return true
|
|
}
|
|
const linkLocal = hostname.match(/^fe([0-9a-f]{2}):/)
|
|
if (linkLocal) {
|
|
const high = parseInt(linkLocal[1], 16)
|
|
if (high >= 0x80 && high <= 0xbf) return true
|
|
}
|
|
}
|
|
|
|
// Block AWS/cloud metadata endpoints
|
|
if (
|
|
hostname === "169.254.169.254" ||
|
|
hostname === "metadata.google.internal"
|
|
) {
|
|
return true
|
|
}
|
|
|
|
// Check for private IPv4 ranges
|
|
const ipv4Match = hostname.match(
|
|
/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,
|
|
)
|
|
if (ipv4Match) {
|
|
const [, a, b] = ipv4Match.map(Number)
|
|
if (a === 10) return true // 10.0.0.0/8
|
|
if (a === 172 && b >= 16 && b <= 31) return true // 172.16.0.0/12
|
|
if (a === 192 && b === 168) return true // 192.168.0.0/16
|
|
if (a === 169 && b === 254) return true // 169.254.0.0/16 (link-local)
|
|
if (a === 127) return true // 127.0.0.0/8 (loopback)
|
|
}
|
|
|
|
// Block common internal hostnames
|
|
if (
|
|
hostname.endsWith(".local") ||
|
|
hostname.endsWith(".internal") ||
|
|
hostname.endsWith(".localhost")
|
|
) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
} catch {
|
|
return true // Invalid URL - block it
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether private URLs are allowed (defaults to true)
|
|
* Set ALLOW_PRIVATE_URLS=false to block private URLs
|
|
*/
|
|
export const allowPrivateUrls = process.env.ALLOW_PRIVATE_URLS !== "false"
|