@node9/proxy 2.16.1 → 2.17.0
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.
- package/dist/cli.js +270 -234
- package/dist/cli.mjs +270 -234
- package/dist/dashboard.mjs +2 -2
- package/dist/index.js +269 -233
- package/dist/index.mjs +269 -233
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2089,30 +2089,213 @@ function analyzeShellCommand(command) {
|
|
|
2089
2089
|
}
|
|
2090
2090
|
return { actions, paths, allTokens };
|
|
2091
2091
|
}
|
|
2092
|
+
function parseComponent(s) {
|
|
2093
|
+
if (!s) return null;
|
|
2094
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
|
|
2095
|
+
if (s === "0") return 0;
|
|
2096
|
+
if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
|
|
2097
|
+
if (/^[1-9][0-9]*$/.test(s)) return Number(s);
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2100
|
+
function parseIpv4(input) {
|
|
2101
|
+
let s = input;
|
|
2102
|
+
if (s.endsWith(".")) s = s.slice(0, -1);
|
|
2103
|
+
if (!s) return null;
|
|
2104
|
+
const parts = s.split(".");
|
|
2105
|
+
if (parts.length > 4) return null;
|
|
2106
|
+
const vals = [];
|
|
2107
|
+
for (const p of parts) {
|
|
2108
|
+
const v = parseComponent(p);
|
|
2109
|
+
if (v === null || !Number.isFinite(v) || v < 0) return null;
|
|
2110
|
+
vals.push(v);
|
|
2111
|
+
}
|
|
2112
|
+
const n = vals.length;
|
|
2113
|
+
for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
|
|
2114
|
+
const last = vals[n - 1];
|
|
2115
|
+
const remainingBytes = 4 - (n - 1);
|
|
2116
|
+
const limit = Math.pow(256, remainingBytes);
|
|
2117
|
+
if (last >= limit) return null;
|
|
2118
|
+
let value = last;
|
|
2119
|
+
for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
|
|
2120
|
+
if (value > 4294967295) return null;
|
|
2121
|
+
return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
2122
|
+
}
|
|
2123
|
+
function expandIpv6(input) {
|
|
2124
|
+
const s = input.toLowerCase();
|
|
2125
|
+
if (!/^[0-9a-f:.]+$/.test(s)) return null;
|
|
2126
|
+
if ((s.match(/::/g) ?? []).length > 1) return null;
|
|
2127
|
+
let head = s;
|
|
2128
|
+
let tailV4 = null;
|
|
2129
|
+
const lastColon = s.lastIndexOf(":");
|
|
2130
|
+
const afterLast = s.slice(lastColon + 1);
|
|
2131
|
+
if (afterLast.includes(".")) {
|
|
2132
|
+
const dotted = parseIpv4(afterLast);
|
|
2133
|
+
if (!dotted) return null;
|
|
2134
|
+
const o = dotted.split(".").map(Number);
|
|
2135
|
+
tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
|
|
2136
|
+
head = s.slice(0, lastColon + 1) + "0";
|
|
2137
|
+
}
|
|
2138
|
+
const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
|
|
2139
|
+
const toGroups = (part) => {
|
|
2140
|
+
if (!part) return [];
|
|
2141
|
+
const out = [];
|
|
2142
|
+
for (const g of part.split(":")) {
|
|
2143
|
+
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
2144
|
+
out.push(parseInt(g, 16));
|
|
2145
|
+
}
|
|
2146
|
+
return out;
|
|
2147
|
+
};
|
|
2148
|
+
const left = toGroups(lhs);
|
|
2149
|
+
if (left === null) return null;
|
|
2150
|
+
let right = [];
|
|
2151
|
+
if (rhs !== null) {
|
|
2152
|
+
const r = toGroups(rhs);
|
|
2153
|
+
if (r === null) return null;
|
|
2154
|
+
right = r;
|
|
2155
|
+
}
|
|
2156
|
+
if (tailV4) {
|
|
2157
|
+
if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
|
|
2158
|
+
else left.splice(left.length - 1, 1, ...tailV4);
|
|
2159
|
+
}
|
|
2160
|
+
const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
|
|
2161
|
+
if (rhs === null && groups.length !== 8) return null;
|
|
2162
|
+
if (rhs !== null && left.length + right.length > 8) return null;
|
|
2163
|
+
if (groups.length !== 8) return null;
|
|
2164
|
+
return groups;
|
|
2165
|
+
}
|
|
2166
|
+
function compressIpv6(g) {
|
|
2167
|
+
let bestStart = -1;
|
|
2168
|
+
let bestLen = 0;
|
|
2169
|
+
let i = 0;
|
|
2170
|
+
while (i < 8) {
|
|
2171
|
+
if (g[i] !== 0) {
|
|
2172
|
+
i++;
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
let j = i;
|
|
2176
|
+
while (j < 8 && g[j] === 0) j++;
|
|
2177
|
+
if (j - i > bestLen) {
|
|
2178
|
+
bestLen = j - i;
|
|
2179
|
+
bestStart = i;
|
|
2180
|
+
}
|
|
2181
|
+
i = j;
|
|
2182
|
+
}
|
|
2183
|
+
const hex = g.map((x) => x.toString(16));
|
|
2184
|
+
if (bestLen < 2) return hex.join(":");
|
|
2185
|
+
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
2186
|
+
}
|
|
2187
|
+
function normalizeIpLiteral(host) {
|
|
2188
|
+
try {
|
|
2189
|
+
if (typeof host !== "string") return null;
|
|
2190
|
+
let s = host.trim();
|
|
2191
|
+
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
2192
|
+
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
2193
|
+
const zone = s.indexOf("%");
|
|
2194
|
+
if (zone >= 0) s = s.slice(0, zone);
|
|
2195
|
+
if (!s) return null;
|
|
2196
|
+
if (s.includes(":")) {
|
|
2197
|
+
const g = expandIpv6(s);
|
|
2198
|
+
if (!g) return null;
|
|
2199
|
+
const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
|
|
2200
|
+
if (mapped) {
|
|
2201
|
+
return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
|
|
2202
|
+
}
|
|
2203
|
+
return compressIpv6(g);
|
|
2204
|
+
}
|
|
2205
|
+
return parseIpv4(s);
|
|
2206
|
+
} catch {
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
function isStrictGatedTier(tier) {
|
|
2211
|
+
return STRICT_TIERS.has(tier);
|
|
2212
|
+
}
|
|
2213
|
+
function ssrfReason(m, asWritten) {
|
|
2214
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
2215
|
+
}
|
|
2216
|
+
function classifySsrf(host) {
|
|
2217
|
+
try {
|
|
2218
|
+
if (typeof host !== "string" || !host) return null;
|
|
2219
|
+
const lower = host.trim().toLowerCase().replace(/\.$/, "");
|
|
2220
|
+
const ip = normalizeIpLiteral(host);
|
|
2221
|
+
if (ip === null) {
|
|
2222
|
+
return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
2223
|
+
}
|
|
2224
|
+
const hit = (tier, overridable) => ({
|
|
2225
|
+
tier,
|
|
2226
|
+
overridable,
|
|
2227
|
+
kind: "address",
|
|
2228
|
+
normalized: ip
|
|
2229
|
+
});
|
|
2230
|
+
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
2231
|
+
const o = v4Octets(ip);
|
|
2232
|
+
if (o) {
|
|
2233
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
2234
|
+
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
2235
|
+
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
2236
|
+
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
2237
|
+
if (o[0] === 127) return hit("private", true);
|
|
2238
|
+
if (o[0] === 10) return hit("private", true);
|
|
2239
|
+
if (o[0] === 192 && o[1] === 168) return hit("private", true);
|
|
2240
|
+
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
|
|
2241
|
+
return null;
|
|
2242
|
+
}
|
|
2243
|
+
const g = expandIpv6(ip);
|
|
2244
|
+
if (!g) return null;
|
|
2245
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
2246
|
+
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
2247
|
+
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
2248
|
+
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
2249
|
+
return null;
|
|
2250
|
+
} catch {
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function ssrfFloor(tokens, opts = {}) {
|
|
2255
|
+
const exempt2 = new Set(
|
|
2256
|
+
(opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
|
|
2257
|
+
);
|
|
2258
|
+
for (const { token, binary } of tokens) {
|
|
2259
|
+
const m = classifySsrf(token);
|
|
2260
|
+
if (!m) continue;
|
|
2261
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2262
|
+
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2263
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
2264
|
+
}
|
|
2265
|
+
return null;
|
|
2266
|
+
}
|
|
2092
2267
|
function hostMatches(host, pattern) {
|
|
2093
|
-
const
|
|
2094
|
-
const p = pattern.toLowerCase().trim();
|
|
2268
|
+
const p = pattern.trim().toLowerCase();
|
|
2095
2269
|
if (!p) return false;
|
|
2096
2270
|
if (p === "*") return true;
|
|
2271
|
+
const h = canonicalHost(host);
|
|
2097
2272
|
if (p.startsWith("*.")) {
|
|
2098
|
-
const suffix = p.slice(2);
|
|
2273
|
+
const suffix = canonicalHost(p.slice(2));
|
|
2099
2274
|
return h === suffix || h.endsWith("." + suffix);
|
|
2100
2275
|
}
|
|
2101
|
-
return h === p;
|
|
2276
|
+
return h === canonicalHost(p);
|
|
2102
2277
|
}
|
|
2103
2278
|
function matchesAny(host, patterns) {
|
|
2104
|
-
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
2279
|
+
for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
|
|
2105
2280
|
return false;
|
|
2106
2281
|
}
|
|
2282
|
+
function isUniqueLocalV6(host) {
|
|
2283
|
+
const ip = normalizeIpLiteral(host);
|
|
2284
|
+
if (!ip || !ip.includes(":")) return false;
|
|
2285
|
+
const g = expandIpv6(ip);
|
|
2286
|
+
return g !== null && (g[0] & 65024) === 64512;
|
|
2287
|
+
}
|
|
2107
2288
|
function isPrivateHost(host) {
|
|
2108
|
-
const h = host.toLowerCase();
|
|
2109
|
-
|
|
2110
|
-
if (
|
|
2111
|
-
if (
|
|
2112
|
-
if (
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2289
|
+
const h = host.trim().toLowerCase().replace(/\.$/, "");
|
|
2290
|
+
const m = classifySsrf(h);
|
|
2291
|
+
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
2292
|
+
if (h === "localhost") return true;
|
|
2293
|
+
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
2294
|
+
return isUniqueLocalV6(h);
|
|
2295
|
+
}
|
|
2296
|
+
function canonicalHost(host) {
|
|
2297
|
+
const ip = normalizeIpLiteral(host);
|
|
2298
|
+
return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
|
|
2116
2299
|
}
|
|
2117
2300
|
function evaluateEgress(dests, policy) {
|
|
2118
2301
|
if (!policy.enabled) return null;
|
|
@@ -2332,181 +2515,6 @@ function extractAllSshHosts(tokens) {
|
|
|
2332
2515
|
}
|
|
2333
2516
|
return [...hosts].filter(Boolean);
|
|
2334
2517
|
}
|
|
2335
|
-
function parseComponent(s) {
|
|
2336
|
-
if (!s) return null;
|
|
2337
|
-
if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
|
|
2338
|
-
if (s === "0") return 0;
|
|
2339
|
-
if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
|
|
2340
|
-
if (/^[1-9][0-9]*$/.test(s)) return Number(s);
|
|
2341
|
-
return null;
|
|
2342
|
-
}
|
|
2343
|
-
function parseIpv4(input) {
|
|
2344
|
-
let s = input;
|
|
2345
|
-
if (s.endsWith(".")) s = s.slice(0, -1);
|
|
2346
|
-
if (!s) return null;
|
|
2347
|
-
const parts = s.split(".");
|
|
2348
|
-
if (parts.length > 4) return null;
|
|
2349
|
-
const vals = [];
|
|
2350
|
-
for (const p of parts) {
|
|
2351
|
-
const v = parseComponent(p);
|
|
2352
|
-
if (v === null || !Number.isFinite(v) || v < 0) return null;
|
|
2353
|
-
vals.push(v);
|
|
2354
|
-
}
|
|
2355
|
-
const n = vals.length;
|
|
2356
|
-
for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
|
|
2357
|
-
const last = vals[n - 1];
|
|
2358
|
-
const remainingBytes = 4 - (n - 1);
|
|
2359
|
-
const limit = Math.pow(256, remainingBytes);
|
|
2360
|
-
if (last >= limit) return null;
|
|
2361
|
-
let value = last;
|
|
2362
|
-
for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
|
|
2363
|
-
if (value > 4294967295) return null;
|
|
2364
|
-
return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
2365
|
-
}
|
|
2366
|
-
function expandIpv6(input) {
|
|
2367
|
-
const s = input.toLowerCase();
|
|
2368
|
-
if (!/^[0-9a-f:.]+$/.test(s)) return null;
|
|
2369
|
-
if ((s.match(/::/g) ?? []).length > 1) return null;
|
|
2370
|
-
let head = s;
|
|
2371
|
-
let tailV4 = null;
|
|
2372
|
-
const lastColon = s.lastIndexOf(":");
|
|
2373
|
-
const afterLast = s.slice(lastColon + 1);
|
|
2374
|
-
if (afterLast.includes(".")) {
|
|
2375
|
-
const dotted = parseIpv4(afterLast);
|
|
2376
|
-
if (!dotted) return null;
|
|
2377
|
-
const o = dotted.split(".").map(Number);
|
|
2378
|
-
tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
|
|
2379
|
-
head = s.slice(0, lastColon + 1) + "0";
|
|
2380
|
-
}
|
|
2381
|
-
const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
|
|
2382
|
-
const toGroups = (part) => {
|
|
2383
|
-
if (!part) return [];
|
|
2384
|
-
const out = [];
|
|
2385
|
-
for (const g of part.split(":")) {
|
|
2386
|
-
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
2387
|
-
out.push(parseInt(g, 16));
|
|
2388
|
-
}
|
|
2389
|
-
return out;
|
|
2390
|
-
};
|
|
2391
|
-
const left = toGroups(lhs);
|
|
2392
|
-
if (left === null) return null;
|
|
2393
|
-
let right = [];
|
|
2394
|
-
if (rhs !== null) {
|
|
2395
|
-
const r = toGroups(rhs);
|
|
2396
|
-
if (r === null) return null;
|
|
2397
|
-
right = r;
|
|
2398
|
-
}
|
|
2399
|
-
if (tailV4) {
|
|
2400
|
-
if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
|
|
2401
|
-
else left.splice(left.length - 1, 1, ...tailV4);
|
|
2402
|
-
}
|
|
2403
|
-
const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
|
|
2404
|
-
if (rhs === null && groups.length !== 8) return null;
|
|
2405
|
-
if (rhs !== null && left.length + right.length > 8) return null;
|
|
2406
|
-
if (groups.length !== 8) return null;
|
|
2407
|
-
return groups;
|
|
2408
|
-
}
|
|
2409
|
-
function compressIpv6(g) {
|
|
2410
|
-
let bestStart = -1;
|
|
2411
|
-
let bestLen = 0;
|
|
2412
|
-
let i = 0;
|
|
2413
|
-
while (i < 8) {
|
|
2414
|
-
if (g[i] !== 0) {
|
|
2415
|
-
i++;
|
|
2416
|
-
continue;
|
|
2417
|
-
}
|
|
2418
|
-
let j = i;
|
|
2419
|
-
while (j < 8 && g[j] === 0) j++;
|
|
2420
|
-
if (j - i > bestLen) {
|
|
2421
|
-
bestLen = j - i;
|
|
2422
|
-
bestStart = i;
|
|
2423
|
-
}
|
|
2424
|
-
i = j;
|
|
2425
|
-
}
|
|
2426
|
-
const hex = g.map((x) => x.toString(16));
|
|
2427
|
-
if (bestLen < 2) return hex.join(":");
|
|
2428
|
-
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
2429
|
-
}
|
|
2430
|
-
function normalizeIpLiteral(host) {
|
|
2431
|
-
try {
|
|
2432
|
-
if (typeof host !== "string") return null;
|
|
2433
|
-
let s = host.trim();
|
|
2434
|
-
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
2435
|
-
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
2436
|
-
const zone = s.indexOf("%");
|
|
2437
|
-
if (zone >= 0) s = s.slice(0, zone);
|
|
2438
|
-
if (!s) return null;
|
|
2439
|
-
if (s.includes(":")) {
|
|
2440
|
-
const g = expandIpv6(s);
|
|
2441
|
-
if (!g) return null;
|
|
2442
|
-
const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
|
|
2443
|
-
if (mapped) {
|
|
2444
|
-
return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
|
|
2445
|
-
}
|
|
2446
|
-
return compressIpv6(g);
|
|
2447
|
-
}
|
|
2448
|
-
return parseIpv4(s);
|
|
2449
|
-
} catch {
|
|
2450
|
-
return null;
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
function isStrictGatedTier(tier) {
|
|
2454
|
-
return STRICT_TIERS.has(tier);
|
|
2455
|
-
}
|
|
2456
|
-
function ssrfReason(m, asWritten) {
|
|
2457
|
-
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
2458
|
-
}
|
|
2459
|
-
function classifySsrf(host) {
|
|
2460
|
-
try {
|
|
2461
|
-
if (typeof host !== "string" || !host) return null;
|
|
2462
|
-
const lower = host.trim().toLowerCase().replace(/\.$/, "");
|
|
2463
|
-
const ip = normalizeIpLiteral(host);
|
|
2464
|
-
if (ip === null) {
|
|
2465
|
-
return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
2466
|
-
}
|
|
2467
|
-
const hit = (tier, overridable) => ({
|
|
2468
|
-
tier,
|
|
2469
|
-
overridable,
|
|
2470
|
-
kind: "address",
|
|
2471
|
-
normalized: ip
|
|
2472
|
-
});
|
|
2473
|
-
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
2474
|
-
const o = v4Octets(ip);
|
|
2475
|
-
if (o) {
|
|
2476
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
2477
|
-
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
2478
|
-
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
2479
|
-
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
2480
|
-
if (o[0] === 127) return hit("private", true);
|
|
2481
|
-
if (o[0] === 10) return hit("private", true);
|
|
2482
|
-
if (o[0] === 192 && o[1] === 168) return hit("private", true);
|
|
2483
|
-
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
|
|
2484
|
-
return null;
|
|
2485
|
-
}
|
|
2486
|
-
const g = expandIpv6(ip);
|
|
2487
|
-
if (!g) return null;
|
|
2488
|
-
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
2489
|
-
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
2490
|
-
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
2491
|
-
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
2492
|
-
return null;
|
|
2493
|
-
} catch {
|
|
2494
|
-
return null;
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
function ssrfFloor(tokens, opts = {}) {
|
|
2498
|
-
const exempt2 = new Set(
|
|
2499
|
-
(opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
|
|
2500
|
-
);
|
|
2501
|
-
for (const { token, binary } of tokens) {
|
|
2502
|
-
const m = classifySsrf(token);
|
|
2503
|
-
if (!m) continue;
|
|
2504
|
-
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
2505
|
-
if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
|
|
2506
|
-
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
2507
|
-
}
|
|
2508
|
-
return null;
|
|
2509
|
-
}
|
|
2510
2518
|
function bareToolName(toolName) {
|
|
2511
2519
|
const parts = toolName.split("__");
|
|
2512
2520
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
@@ -2540,6 +2548,22 @@ function hostOf(value) {
|
|
|
2540
2548
|
return null;
|
|
2541
2549
|
}
|
|
2542
2550
|
}
|
|
2551
|
+
function extractToolDestinations(toolName, args) {
|
|
2552
|
+
try {
|
|
2553
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
2554
|
+
if (!paths) return [];
|
|
2555
|
+
const out = [];
|
|
2556
|
+
for (const path78 of paths) {
|
|
2557
|
+
for (const value of valuesAt(args, path78)) {
|
|
2558
|
+
const host = hostOf(value);
|
|
2559
|
+
if (host) out.push({ host, binary: toolName, raw: value });
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return out;
|
|
2563
|
+
} catch {
|
|
2564
|
+
return [];
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2543
2567
|
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
2544
2568
|
try {
|
|
2545
2569
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
@@ -2641,6 +2665,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
|
2641
2665
|
tier: 3
|
|
2642
2666
|
};
|
|
2643
2667
|
}
|
|
2668
|
+
function egressPolicyVerdict(eg) {
|
|
2669
|
+
return {
|
|
2670
|
+
decision: eg.verdict,
|
|
2671
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2672
|
+
reason: eg.reason,
|
|
2673
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2674
|
+
ruleDescription: eg.reason,
|
|
2675
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2644
2678
|
async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
|
|
2645
2679
|
const { agent, cwd, activeEnvironment } = context;
|
|
2646
2680
|
const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
|
|
@@ -2675,7 +2709,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2675
2709
|
};
|
|
2676
2710
|
}
|
|
2677
2711
|
}
|
|
2678
|
-
|
|
2712
|
+
const pendingToolEgress = config.policy.egress?.enabled ? (() => {
|
|
2713
|
+
const dests = extractToolDestinations(toolName, args);
|
|
2714
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
2715
|
+
return eg ? egressPolicyVerdict(eg) : void 0;
|
|
2716
|
+
})() : void 0;
|
|
2717
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) {
|
|
2718
|
+
return pendingToolEgress ?? { decision: "allow" };
|
|
2719
|
+
}
|
|
2679
2720
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2680
2721
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2681
2722
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
@@ -2764,6 +2805,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2764
2805
|
}
|
|
2765
2806
|
let allTokens = [];
|
|
2766
2807
|
let pathTokens = [];
|
|
2808
|
+
if (pendingToolEgress) return pendingToolEgress;
|
|
2767
2809
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
2768
2810
|
if (shellCommand) {
|
|
2769
2811
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
@@ -2829,16 +2871,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2829
2871
|
const dests = extractShellDestinations(shellCommand);
|
|
2830
2872
|
if (dests.length > 0) {
|
|
2831
2873
|
const eg = evaluateEgress(dests, config.policy.egress);
|
|
2832
|
-
if (eg)
|
|
2833
|
-
return {
|
|
2834
|
-
decision: eg.verdict,
|
|
2835
|
-
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2836
|
-
reason: eg.reason,
|
|
2837
|
-
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2838
|
-
ruleDescription: eg.reason,
|
|
2839
|
-
tier: eg.verdict === "block" ? 3 : 4
|
|
2840
|
-
};
|
|
2841
|
-
}
|
|
2874
|
+
if (eg) return egressPolicyVerdict(eg);
|
|
2842
2875
|
}
|
|
2843
2876
|
}
|
|
2844
2877
|
const firstToken = analyzed.actions[0] ?? "";
|
|
@@ -3712,7 +3745,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
3712
3745
|
}
|
|
3713
3746
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
3714
3747
|
}
|
|
3715
|
-
var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN,
|
|
3748
|
+
var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, TERMINAL_ESCAPE_RE, CONTROL_CHAR_RE, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
|
|
3716
3749
|
var init_dist = __esm({
|
|
3717
3750
|
"packages/policy-engine/dist/index.mjs"() {
|
|
3718
3751
|
"use strict";
|
|
@@ -5157,6 +5190,36 @@ var init_dist = __esm({
|
|
|
5157
5190
|
positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
5158
5191
|
NONE = { kind: "none" };
|
|
5159
5192
|
UNKNOWN = { kind: "unknown" };
|
|
5193
|
+
SSRF_MAX_HOST = 253;
|
|
5194
|
+
METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
5195
|
+
"169.254.169.254",
|
|
5196
|
+
// AWS / Azure / DigitalOcean / OpenStack IMDS
|
|
5197
|
+
"169.254.170.2",
|
|
5198
|
+
// AWS ECS task role
|
|
5199
|
+
"168.63.129.16",
|
|
5200
|
+
// Azure WireServer
|
|
5201
|
+
"fd00:ec2::254",
|
|
5202
|
+
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
5203
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
5204
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
5205
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
5206
|
+
// and it is the reason relaxing cgnat is safe.
|
|
5207
|
+
"100.100.100.200"
|
|
5208
|
+
]);
|
|
5209
|
+
STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
5210
|
+
METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
5211
|
+
v4Octets = (a) => {
|
|
5212
|
+
const p = a.split(".");
|
|
5213
|
+
return p.length === 4 ? p.map(Number) : null;
|
|
5214
|
+
};
|
|
5215
|
+
TIER_REASON = {
|
|
5216
|
+
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
5217
|
+
"link-local": "a link-local address",
|
|
5218
|
+
multicast: "a multicast address",
|
|
5219
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
5220
|
+
cgnat: "a carrier-grade NAT address",
|
|
5221
|
+
private: "a loopback or private address"
|
|
5222
|
+
};
|
|
5160
5223
|
DEFAULT_EGRESS_ALLOWLIST = [
|
|
5161
5224
|
// node9's own control plane (api, app, dev-api, staging and the apex).
|
|
5162
5225
|
// Without it, turning egress on asks the user to approve node9 itself.
|
|
@@ -5180,6 +5243,7 @@ var init_dist = __esm({
|
|
|
5180
5243
|
"deb.debian.org",
|
|
5181
5244
|
"*.ubuntu.com"
|
|
5182
5245
|
];
|
|
5246
|
+
PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
|
|
5183
5247
|
SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
|
|
5184
5248
|
SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
5185
5249
|
"curl",
|
|
@@ -5303,36 +5367,6 @@ var init_dist = __esm({
|
|
|
5303
5367
|
socat: /* @__PURE__ */ new Set([])
|
|
5304
5368
|
// socat uses address syntax, not flags — no value-flags
|
|
5305
5369
|
};
|
|
5306
|
-
SSRF_MAX_HOST = 253;
|
|
5307
|
-
METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
5308
|
-
"169.254.169.254",
|
|
5309
|
-
// AWS / Azure / DigitalOcean / OpenStack IMDS
|
|
5310
|
-
"169.254.170.2",
|
|
5311
|
-
// AWS ECS task role
|
|
5312
|
-
"168.63.129.16",
|
|
5313
|
-
// Azure WireServer
|
|
5314
|
-
"fd00:ec2::254",
|
|
5315
|
-
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
5316
|
-
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
5317
|
-
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
5318
|
-
// live credential endpoint. It has to be named here, above the range check,
|
|
5319
|
-
// and it is the reason relaxing cgnat is safe.
|
|
5320
|
-
"100.100.100.200"
|
|
5321
|
-
]);
|
|
5322
|
-
STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
5323
|
-
METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
5324
|
-
v4Octets = (a) => {
|
|
5325
|
-
const p = a.split(".");
|
|
5326
|
-
return p.length === 4 ? p.map(Number) : null;
|
|
5327
|
-
};
|
|
5328
|
-
TIER_REASON = {
|
|
5329
|
-
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
5330
|
-
"link-local": "a link-local address",
|
|
5331
|
-
multicast: "a multicast address",
|
|
5332
|
-
unspecified: "the unspecified address, which reaches this host",
|
|
5333
|
-
cgnat: "a carrier-grade NAT address",
|
|
5334
|
-
private: "a loopback or private address"
|
|
5335
|
-
};
|
|
5336
5370
|
DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
5337
5371
|
["webfetch", ["url"]],
|
|
5338
5372
|
["fetch", ["url", "uri"]],
|
|
@@ -9940,8 +9974,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
9940
9974
|
};
|
|
9941
9975
|
}
|
|
9942
9976
|
}
|
|
9977
|
+
const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
|
|
9978
|
+
const judge = !isIgnoredTool2(toolName) || declaredEgress;
|
|
9943
9979
|
if (isObserveMode) {
|
|
9944
|
-
if (
|
|
9980
|
+
if (judge) {
|
|
9945
9981
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
9946
9982
|
const wouldBlock = policyResult.decision === "block";
|
|
9947
9983
|
if (!isManual)
|
|
@@ -9966,7 +10002,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
9966
10002
|
return { approved: true, checkedBy: "audit" };
|
|
9967
10003
|
}
|
|
9968
10004
|
if (config.settings.mode === "audit") {
|
|
9969
|
-
if (
|
|
10005
|
+
if (judge) {
|
|
9970
10006
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
9971
10007
|
if (policyResult.decision === "review") {
|
|
9972
10008
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
@@ -10005,9 +10041,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
10005
10041
|
appPermReviewTool = bareTool;
|
|
10006
10042
|
}
|
|
10007
10043
|
}
|
|
10008
|
-
if (!taintWarning &&
|
|
10044
|
+
if (!taintWarning && judge) {
|
|
10009
10045
|
const ld = config.policy.loopDetection;
|
|
10010
|
-
if (ld.enabled && !appPermReview) {
|
|
10046
|
+
if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
|
|
10011
10047
|
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
10012
10048
|
if (loopResult.looping) {
|
|
10013
10049
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|