@fudrouter/fsrouter 0.6.112 → 0.6.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// SSRF guard: block internal/private/metadata targets for server-side fetch.
|
|
2
|
+
// Mirrors 9router's assertPublicUrl so custom provider nodes can't be pointed
|
|
3
|
+
// at localhost / cloud metadata endpoints (e.g. 169.254.169.254).
|
|
4
|
+
|
|
5
|
+
const BLOCKED_HOSTNAMES = new Set(["localhost", "ip6-localhost", "ip6-loopback"]);
|
|
6
|
+
const BLOCKED_SUFFIXES = [".internal", ".local", ".localhost"];
|
|
7
|
+
|
|
8
|
+
function ipv4ToInt(host) {
|
|
9
|
+
const parts = host.split(".");
|
|
10
|
+
if (parts.length !== 4) return null;
|
|
11
|
+
let value = 0;
|
|
12
|
+
for (const part of parts) {
|
|
13
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
14
|
+
const octet = Number(part);
|
|
15
|
+
if (octet > 255) return null;
|
|
16
|
+
value = value * 256 + octet;
|
|
17
|
+
}
|
|
18
|
+
return value >>> 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const BLOCKED_V4_RANGES = [
|
|
22
|
+
[ipv4ToInt("0.0.0.0"), 8],
|
|
23
|
+
[ipv4ToInt("10.0.0.0"), 8],
|
|
24
|
+
[ipv4ToInt("127.0.0.0"), 8],
|
|
25
|
+
[ipv4ToInt("169.254.0.0"), 16],
|
|
26
|
+
[ipv4ToInt("172.16.0.0"), 12],
|
|
27
|
+
[ipv4ToInt("192.168.0.0"), 16],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function isBlockedIpv4(host) {
|
|
31
|
+
const ip = ipv4ToInt(host);
|
|
32
|
+
if (ip === null) return false;
|
|
33
|
+
return BLOCKED_V4_RANGES.some(([base, bits]) => {
|
|
34
|
+
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
|
|
35
|
+
return (ip & mask) === (base & mask);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isBlockedIpv6(host) {
|
|
40
|
+
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
41
|
+
const v4Mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
42
|
+
if (v4Mapped) return isBlockedIpv4(v4Mapped[1]);
|
|
43
|
+
if (h === "::1" || h === "::") return true;
|
|
44
|
+
return h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Throw if URL targets a non-public host. Caller should map to 400.
|
|
48
|
+
export function assertPublicUrl(rawUrl) {
|
|
49
|
+
const parsed = new URL(rawUrl);
|
|
50
|
+
const host = parsed.hostname.toLowerCase();
|
|
51
|
+
if (BLOCKED_HOSTNAMES.has(host)) throw new Error("Blocked host");
|
|
52
|
+
if (BLOCKED_SUFFIXES.some((s) => host.endsWith(s))) throw new Error("Blocked host suffix");
|
|
53
|
+
if (isBlockedIpv4(host) || isBlockedIpv6(host)) throw new Error("Blocked private IP");
|
|
54
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { assertPublicUrl } from "../../../lib/ssrfGuard.js";
|
|
2
|
+
// Fetch with timeout wrapper — uses AbortController (reliably aborts the
|
|
3
|
+
// underlying request instead of just racing a dangling promise that can hang
|
|
4
|
+
// the whole HTTP handler / make the browser show "Network error").
|
|
2
5
|
const fetchWithTimeout = (url, options, timeout = 10000) => {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
]);
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const t = setTimeout(() => controller.abort(), timeout);
|
|
8
|
+
return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(t));
|
|
7
9
|
};
|
|
8
10
|
// Validate URL format
|
|
9
11
|
const isValidUrl = (url) => {
|
|
@@ -67,6 +69,13 @@ export async function POST_handler(req, res) {
|
|
|
67
69
|
if (!isValidUrl(baseUrl)) {
|
|
68
70
|
return res.status(400).json({ error: "Invalid URL format" });
|
|
69
71
|
}
|
|
72
|
+
// SSRF guard: block localhost / private / cloud-metadata targets
|
|
73
|
+
try {
|
|
74
|
+
assertPublicUrl(baseUrl);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return res.status(400).json({ error: "URL not allowed (internal/private address)" });
|
|
78
|
+
}
|
|
70
79
|
// Custom Embedding Validation - test POST /embeddings directly
|
|
71
80
|
if (type === "custom-embedding") {
|
|
72
81
|
const normalizedBase = baseUrl.trim().replace(/\/$/, "");
|