@node9/proxy 2.16.0 → 2.16.2

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.mjs CHANGED
@@ -2100,6 +2100,181 @@ function analyzeShellCommand(command) {
2100
2100
  }
2101
2101
  return { actions, paths, allTokens };
2102
2102
  }
2103
+ function parseComponent(s) {
2104
+ if (!s) return null;
2105
+ if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
2106
+ if (s === "0") return 0;
2107
+ if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
2108
+ if (/^[1-9][0-9]*$/.test(s)) return Number(s);
2109
+ return null;
2110
+ }
2111
+ function parseIpv4(input) {
2112
+ let s = input;
2113
+ if (s.endsWith(".")) s = s.slice(0, -1);
2114
+ if (!s) return null;
2115
+ const parts = s.split(".");
2116
+ if (parts.length > 4) return null;
2117
+ const vals = [];
2118
+ for (const p of parts) {
2119
+ const v = parseComponent(p);
2120
+ if (v === null || !Number.isFinite(v) || v < 0) return null;
2121
+ vals.push(v);
2122
+ }
2123
+ const n = vals.length;
2124
+ for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
2125
+ const last = vals[n - 1];
2126
+ const remainingBytes = 4 - (n - 1);
2127
+ const limit = Math.pow(256, remainingBytes);
2128
+ if (last >= limit) return null;
2129
+ let value = last;
2130
+ for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
2131
+ if (value > 4294967295) return null;
2132
+ return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
2133
+ }
2134
+ function expandIpv6(input) {
2135
+ const s = input.toLowerCase();
2136
+ if (!/^[0-9a-f:.]+$/.test(s)) return null;
2137
+ if ((s.match(/::/g) ?? []).length > 1) return null;
2138
+ let head = s;
2139
+ let tailV4 = null;
2140
+ const lastColon = s.lastIndexOf(":");
2141
+ const afterLast = s.slice(lastColon + 1);
2142
+ if (afterLast.includes(".")) {
2143
+ const dotted = parseIpv4(afterLast);
2144
+ if (!dotted) return null;
2145
+ const o = dotted.split(".").map(Number);
2146
+ tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
2147
+ head = s.slice(0, lastColon + 1) + "0";
2148
+ }
2149
+ const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
2150
+ const toGroups = (part) => {
2151
+ if (!part) return [];
2152
+ const out = [];
2153
+ for (const g of part.split(":")) {
2154
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
2155
+ out.push(parseInt(g, 16));
2156
+ }
2157
+ return out;
2158
+ };
2159
+ const left = toGroups(lhs);
2160
+ if (left === null) return null;
2161
+ let right = [];
2162
+ if (rhs !== null) {
2163
+ const r = toGroups(rhs);
2164
+ if (r === null) return null;
2165
+ right = r;
2166
+ }
2167
+ if (tailV4) {
2168
+ if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
2169
+ else left.splice(left.length - 1, 1, ...tailV4);
2170
+ }
2171
+ const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
2172
+ if (rhs === null && groups.length !== 8) return null;
2173
+ if (rhs !== null && left.length + right.length > 8) return null;
2174
+ if (groups.length !== 8) return null;
2175
+ return groups;
2176
+ }
2177
+ function compressIpv6(g) {
2178
+ let bestStart = -1;
2179
+ let bestLen = 0;
2180
+ let i = 0;
2181
+ while (i < 8) {
2182
+ if (g[i] !== 0) {
2183
+ i++;
2184
+ continue;
2185
+ }
2186
+ let j = i;
2187
+ while (j < 8 && g[j] === 0) j++;
2188
+ if (j - i > bestLen) {
2189
+ bestLen = j - i;
2190
+ bestStart = i;
2191
+ }
2192
+ i = j;
2193
+ }
2194
+ const hex = g.map((x) => x.toString(16));
2195
+ if (bestLen < 2) return hex.join(":");
2196
+ return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
2197
+ }
2198
+ function normalizeIpLiteral(host) {
2199
+ try {
2200
+ if (typeof host !== "string") return null;
2201
+ let s = host.trim();
2202
+ if (!s || s.length > SSRF_MAX_HOST) return null;
2203
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
2204
+ const zone = s.indexOf("%");
2205
+ if (zone >= 0) s = s.slice(0, zone);
2206
+ if (!s) return null;
2207
+ if (s.includes(":")) {
2208
+ const g = expandIpv6(s);
2209
+ if (!g) return null;
2210
+ const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
2211
+ if (mapped) {
2212
+ return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
2213
+ }
2214
+ return compressIpv6(g);
2215
+ }
2216
+ return parseIpv4(s);
2217
+ } catch {
2218
+ return null;
2219
+ }
2220
+ }
2221
+ function isStrictGatedTier(tier) {
2222
+ return STRICT_TIERS.has(tier);
2223
+ }
2224
+ function ssrfReason(m, asWritten) {
2225
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2226
+ }
2227
+ function classifySsrf(host) {
2228
+ try {
2229
+ if (typeof host !== "string" || !host) return null;
2230
+ const lower = host.trim().toLowerCase().replace(/\.$/, "");
2231
+ const ip = normalizeIpLiteral(host);
2232
+ if (ip === null) {
2233
+ return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
2234
+ }
2235
+ const hit = (tier, overridable) => ({
2236
+ tier,
2237
+ overridable,
2238
+ kind: "address",
2239
+ normalized: ip
2240
+ });
2241
+ if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2242
+ const o = v4Octets(ip);
2243
+ if (o) {
2244
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2245
+ if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2246
+ if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2247
+ if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
2248
+ if (o[0] === 127) return hit("private", true);
2249
+ if (o[0] === 10) return hit("private", true);
2250
+ if (o[0] === 192 && o[1] === 168) return hit("private", true);
2251
+ if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
2252
+ return null;
2253
+ }
2254
+ const g = expandIpv6(ip);
2255
+ if (!g) return null;
2256
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
2257
+ if ((g[0] & 65472) === 65152) return hit("link-local", false);
2258
+ if ((g[0] & 65280) === 65280) return hit("multicast", false);
2259
+ if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
2260
+ return null;
2261
+ } catch {
2262
+ return null;
2263
+ }
2264
+ }
2265
+ function ssrfFloor(tokens, opts = {}) {
2266
+ const exempt2 = new Set(
2267
+ (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
2268
+ );
2269
+ for (const { token, binary } of tokens) {
2270
+ const m = classifySsrf(token);
2271
+ if (!m) continue;
2272
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2273
+ if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2274
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2275
+ }
2276
+ return null;
2277
+ }
2103
2278
  function hostMatches(host, pattern) {
2104
2279
  const h = host.toLowerCase();
2105
2280
  const p = pattern.toLowerCase().trim();
@@ -2115,15 +2290,19 @@ function matchesAny(host, patterns) {
2115
2290
  for (const p of patterns) if (hostMatches(host, p)) return true;
2116
2291
  return false;
2117
2292
  }
2293
+ function isUniqueLocalV6(host) {
2294
+ const ip = normalizeIpLiteral(host);
2295
+ if (!ip || !ip.includes(":")) return false;
2296
+ const g = expandIpv6(ip);
2297
+ return g !== null && (g[0] & 65024) === 64512;
2298
+ }
2118
2299
  function isPrivateHost(host) {
2119
- const h = host.toLowerCase();
2120
- if (h === "localhost" || h === "0.0.0.0") return true;
2121
- if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
2122
- if (/^127\./.test(h)) return true;
2123
- if (/^10\./.test(h)) return true;
2124
- if (/^192\.168\./.test(h)) return true;
2125
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
2126
- return false;
2300
+ const h = host.trim().toLowerCase();
2301
+ const m = classifySsrf(h);
2302
+ if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
2303
+ if (h === "localhost") return true;
2304
+ if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
2305
+ return isUniqueLocalV6(h);
2127
2306
  }
2128
2307
  function evaluateEgress(dests, policy) {
2129
2308
  if (!policy.enabled) return null;
@@ -2343,181 +2522,6 @@ function extractAllSshHosts(tokens) {
2343
2522
  }
2344
2523
  return [...hosts].filter(Boolean);
2345
2524
  }
2346
- function parseComponent(s) {
2347
- if (!s) return null;
2348
- if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
2349
- if (s === "0") return 0;
2350
- if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
2351
- if (/^[1-9][0-9]*$/.test(s)) return Number(s);
2352
- return null;
2353
- }
2354
- function parseIpv4(input) {
2355
- let s = input;
2356
- if (s.endsWith(".")) s = s.slice(0, -1);
2357
- if (!s) return null;
2358
- const parts = s.split(".");
2359
- if (parts.length > 4) return null;
2360
- const vals = [];
2361
- for (const p of parts) {
2362
- const v = parseComponent(p);
2363
- if (v === null || !Number.isFinite(v) || v < 0) return null;
2364
- vals.push(v);
2365
- }
2366
- const n = vals.length;
2367
- for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
2368
- const last = vals[n - 1];
2369
- const remainingBytes = 4 - (n - 1);
2370
- const limit = Math.pow(256, remainingBytes);
2371
- if (last >= limit) return null;
2372
- let value = last;
2373
- for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
2374
- if (value > 4294967295) return null;
2375
- return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
2376
- }
2377
- function expandIpv6(input) {
2378
- const s = input.toLowerCase();
2379
- if (!/^[0-9a-f:.]+$/.test(s)) return null;
2380
- if ((s.match(/::/g) ?? []).length > 1) return null;
2381
- let head = s;
2382
- let tailV4 = null;
2383
- const lastColon = s.lastIndexOf(":");
2384
- const afterLast = s.slice(lastColon + 1);
2385
- if (afterLast.includes(".")) {
2386
- const dotted = parseIpv4(afterLast);
2387
- if (!dotted) return null;
2388
- const o = dotted.split(".").map(Number);
2389
- tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
2390
- head = s.slice(0, lastColon + 1) + "0";
2391
- }
2392
- const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
2393
- const toGroups = (part) => {
2394
- if (!part) return [];
2395
- const out = [];
2396
- for (const g of part.split(":")) {
2397
- if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
2398
- out.push(parseInt(g, 16));
2399
- }
2400
- return out;
2401
- };
2402
- const left = toGroups(lhs);
2403
- if (left === null) return null;
2404
- let right = [];
2405
- if (rhs !== null) {
2406
- const r = toGroups(rhs);
2407
- if (r === null) return null;
2408
- right = r;
2409
- }
2410
- if (tailV4) {
2411
- if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
2412
- else left.splice(left.length - 1, 1, ...tailV4);
2413
- }
2414
- const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
2415
- if (rhs === null && groups.length !== 8) return null;
2416
- if (rhs !== null && left.length + right.length > 8) return null;
2417
- if (groups.length !== 8) return null;
2418
- return groups;
2419
- }
2420
- function compressIpv6(g) {
2421
- let bestStart = -1;
2422
- let bestLen = 0;
2423
- let i = 0;
2424
- while (i < 8) {
2425
- if (g[i] !== 0) {
2426
- i++;
2427
- continue;
2428
- }
2429
- let j = i;
2430
- while (j < 8 && g[j] === 0) j++;
2431
- if (j - i > bestLen) {
2432
- bestLen = j - i;
2433
- bestStart = i;
2434
- }
2435
- i = j;
2436
- }
2437
- const hex = g.map((x) => x.toString(16));
2438
- if (bestLen < 2) return hex.join(":");
2439
- return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
2440
- }
2441
- function normalizeIpLiteral(host) {
2442
- try {
2443
- if (typeof host !== "string") return null;
2444
- let s = host.trim();
2445
- if (!s || s.length > SSRF_MAX_HOST) return null;
2446
- if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
2447
- const zone = s.indexOf("%");
2448
- if (zone >= 0) s = s.slice(0, zone);
2449
- if (!s) return null;
2450
- if (s.includes(":")) {
2451
- const g = expandIpv6(s);
2452
- if (!g) return null;
2453
- const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
2454
- if (mapped) {
2455
- return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
2456
- }
2457
- return compressIpv6(g);
2458
- }
2459
- return parseIpv4(s);
2460
- } catch {
2461
- return null;
2462
- }
2463
- }
2464
- function isStrictGatedTier(tier) {
2465
- return STRICT_TIERS.has(tier);
2466
- }
2467
- function ssrfReason(m, asWritten) {
2468
- return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2469
- }
2470
- function classifySsrf(host) {
2471
- try {
2472
- if (typeof host !== "string" || !host) return null;
2473
- const lower = host.trim().toLowerCase().replace(/\.$/, "");
2474
- const ip = normalizeIpLiteral(host);
2475
- if (ip === null) {
2476
- return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
2477
- }
2478
- const hit = (tier, overridable) => ({
2479
- tier,
2480
- overridable,
2481
- kind: "address",
2482
- normalized: ip
2483
- });
2484
- if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2485
- const o = v4Octets(ip);
2486
- if (o) {
2487
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2488
- if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2489
- if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2490
- if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
2491
- if (o[0] === 127) return hit("private", true);
2492
- if (o[0] === 10) return hit("private", true);
2493
- if (o[0] === 192 && o[1] === 168) return hit("private", true);
2494
- if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
2495
- return null;
2496
- }
2497
- const g = expandIpv6(ip);
2498
- if (!g) return null;
2499
- if (g.every((x) => x === 0)) return hit("unspecified", true);
2500
- if ((g[0] & 65472) === 65152) return hit("link-local", false);
2501
- if ((g[0] & 65280) === 65280) return hit("multicast", false);
2502
- if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
2503
- return null;
2504
- } catch {
2505
- return null;
2506
- }
2507
- }
2508
- function ssrfFloor(tokens, opts = {}) {
2509
- const exempt2 = new Set(
2510
- (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
2511
- );
2512
- for (const { token, binary } of tokens) {
2513
- const m = classifySsrf(token);
2514
- if (!m) continue;
2515
- if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2516
- if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2517
- return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2518
- }
2519
- return null;
2520
- }
2521
2525
  function bareToolName(toolName) {
2522
2526
  const parts = toolName.split("__");
2523
2527
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
@@ -3723,7 +3727,7 @@ function* stringValues(obj, depth = 0) {
3723
3727
  }
3724
3728
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3725
3729
  }
3726
- var 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, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, 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;
3730
+ var 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;
3727
3731
  var init_dist = __esm({
3728
3732
  "packages/policy-engine/dist/index.mjs"() {
3729
3733
  "use strict";
@@ -5161,6 +5165,36 @@ var init_dist = __esm({
5161
5165
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
5162
5166
  NONE = { kind: "none" };
5163
5167
  UNKNOWN = { kind: "unknown" };
5168
+ SSRF_MAX_HOST = 253;
5169
+ METADATA_ADDRESSES = /* @__PURE__ */ new Set([
5170
+ "169.254.169.254",
5171
+ // AWS / Azure / DigitalOcean / OpenStack IMDS
5172
+ "169.254.170.2",
5173
+ // AWS ECS task role
5174
+ "168.63.129.16",
5175
+ // Azure WireServer
5176
+ "fd00:ec2::254",
5177
+ // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
5178
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
5179
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
5180
+ // live credential endpoint. It has to be named here, above the range check,
5181
+ // and it is the reason relaxing cgnat is safe.
5182
+ "100.100.100.200"
5183
+ ]);
5184
+ STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
5185
+ METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
5186
+ v4Octets = (a) => {
5187
+ const p = a.split(".");
5188
+ return p.length === 4 ? p.map(Number) : null;
5189
+ };
5190
+ TIER_REASON = {
5191
+ metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
5192
+ "link-local": "a link-local address",
5193
+ multicast: "a multicast address",
5194
+ unspecified: "the unspecified address, which reaches this host",
5195
+ cgnat: "a carrier-grade NAT address",
5196
+ private: "a loopback or private address"
5197
+ };
5164
5198
  DEFAULT_EGRESS_ALLOWLIST = [
5165
5199
  // node9's own control plane (api, app, dev-api, staging and the apex).
5166
5200
  // Without it, turning egress on asks the user to approve node9 itself.
@@ -5184,6 +5218,7 @@ var init_dist = __esm({
5184
5218
  "deb.debian.org",
5185
5219
  "*.ubuntu.com"
5186
5220
  ];
5221
+ PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
5187
5222
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
5188
5223
  SINK_COMMANDS = /* @__PURE__ */ new Set([
5189
5224
  "curl",
@@ -5307,36 +5342,6 @@ var init_dist = __esm({
5307
5342
  socat: /* @__PURE__ */ new Set([])
5308
5343
  // socat uses address syntax, not flags — no value-flags
5309
5344
  };
5310
- SSRF_MAX_HOST = 253;
5311
- METADATA_ADDRESSES = /* @__PURE__ */ new Set([
5312
- "169.254.169.254",
5313
- // AWS / Azure / DigitalOcean / OpenStack IMDS
5314
- "169.254.170.2",
5315
- // AWS ECS task role
5316
- "168.63.129.16",
5317
- // Azure WireServer
5318
- "fd00:ec2::254",
5319
- // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
5320
- // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
5321
- // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
5322
- // live credential endpoint. It has to be named here, above the range check,
5323
- // and it is the reason relaxing cgnat is safe.
5324
- "100.100.100.200"
5325
- ]);
5326
- STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
5327
- METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
5328
- v4Octets = (a) => {
5329
- const p = a.split(".");
5330
- return p.length === 4 ? p.map(Number) : null;
5331
- };
5332
- TIER_REASON = {
5333
- metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
5334
- "link-local": "a link-local address",
5335
- multicast: "a multicast address",
5336
- unspecified: "the unspecified address, which reaches this host",
5337
- cgnat: "a carrier-grade NAT address",
5338
- private: "a loopback or private address"
5339
- };
5340
5345
  DESTINATION_ARGS = /* @__PURE__ */ new Map([
5341
5346
  ["webfetch", ["url"]],
5342
5347
  ["fetch", ["url", "uri"]],
@@ -6838,14 +6843,14 @@ function getCredentials() {
6838
6843
  const creds = JSON.parse(fs4.readFileSync(credPath, "utf-8"));
6839
6844
  const profileName = process.env.NODE9_PROFILE || "default";
6840
6845
  const profile = creds[profileName];
6841
- if (profile?.apiKey) {
6846
+ if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
6842
6847
  return {
6843
6848
  apiKey: profile.apiKey,
6844
6849
  apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
6845
6850
  localOnly: profile.localOnly === true || profileName !== "default"
6846
6851
  };
6847
6852
  }
6848
- if (creds.apiKey) {
6853
+ if (typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
6849
6854
  return {
6850
6855
  apiKey: creds.apiKey,
6851
6856
  apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
@@ -9356,23 +9361,8 @@ var init_read_capped = __esm({
9356
9361
  import fs14 from "fs";
9357
9362
  import os11 from "os";
9358
9363
  import path15 from "path";
9359
- function validateApiUrl2(raw) {
9360
- let u;
9361
- try {
9362
- u = new URL(raw);
9363
- } catch {
9364
- return null;
9365
- }
9366
- if (u.username || u.password) return null;
9367
- if (u.protocol === "https:") return u;
9368
- if (u.protocol === "http:") {
9369
- const h = u.hostname;
9370
- if (h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]") return u;
9371
- }
9372
- return null;
9373
- }
9374
9364
  function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
9375
- const validated = validateApiUrl2(creds.apiUrl);
9365
+ const validated = validateApiUrl(creds.apiUrl);
9376
9366
  if (!validated) {
9377
9367
  try {
9378
9368
  fs14.appendFileSync(
@@ -9553,6 +9543,7 @@ var init_cloud = __esm({
9553
9543
  init_audit();
9554
9544
  init_safe_text();
9555
9545
  init_read_capped();
9546
+ init_api_url();
9556
9547
  DLP_SAMPLE_MAX_LEN = 200;
9557
9548
  DLP_PATTERN_MAX_LEN = 100;
9558
9549
  KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
@@ -21560,37 +21551,13 @@ function resolveSyncIntervalMs(settings) {
21560
21551
  return clamped * 1e3;
21561
21552
  }
21562
21553
  function readCredentials() {
21563
- if (process.env.NODE9_API_KEY) {
21564
- return {
21565
- apiKey: process.env.NODE9_API_KEY,
21566
- apiUrl: process.env.NODE9_API_URL ?? DEFAULT_API_URL2
21567
- };
21568
- }
21569
- try {
21570
- const credPath = path41.join(os36.homedir(), ".node9", "credentials.json");
21571
- const creds = JSON.parse(fs42.readFileSync(credPath, "utf-8"));
21572
- const profileName = process.env.NODE9_PROFILE ?? "default";
21573
- const profile = creds[profileName];
21574
- if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
21575
- return {
21576
- apiKey: profile.apiKey,
21577
- apiUrl: typeof profile.apiUrl === "string" ? (
21578
- // Credentials store the firewall base URL (e.g.
21579
- // `https://api.node9.ai/api/v1/intercept`) so existing CLI
21580
- // calls keep working. Sync lives at `/intercept/policies/sync`
21581
- // — append the suffix when the stored URL ends in `/intercept`.
21582
- // Anything else is taken as-is so users can override the full
21583
- // URL via NODE9_API_URL or a non-standard apiUrl.
21584
- /\/intercept$/.test(profile.apiUrl) ? profile.apiUrl + "/policies/sync" : profile.apiUrl
21585
- ) : DEFAULT_API_URL2
21586
- };
21587
- }
21588
- if (typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
21589
- return { apiKey: creds.apiKey, apiUrl: DEFAULT_API_URL2 };
21590
- }
21591
- } catch {
21592
- }
21593
- return null;
21554
+ const creds = getCredentials();
21555
+ if (!creds) return null;
21556
+ const base = creds.apiUrl.replace(/\/+$/, "");
21557
+ return {
21558
+ apiKey: creds.apiKey,
21559
+ apiUrl: /\/intercept$/.test(base) ? base + "/policies/sync" : base
21560
+ };
21594
21561
  }
21595
21562
  function readCachedEtag() {
21596
21563
  try {
@@ -22208,7 +22175,7 @@ function startForensicBroadcast() {
22208
22175
  const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
22209
22176
  recurring.unref();
22210
22177
  }
22211
- var FINDING_TO_SIGNAL3, rulesCacheFile, rulesCacheBackupFile, DEFAULT_API_URL2, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
22178
+ var FINDING_TO_SIGNAL3, rulesCacheFile, rulesCacheBackupFile, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, syncHealthFile, STALE_MIN_MS, STALE_MAX_MS, STALE_FACTOR, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
22212
22179
  var init_sync = __esm({
22213
22180
  "src/daemon/sync.ts"() {
22214
22181
  "use strict";
@@ -22227,7 +22194,6 @@ var init_sync = __esm({
22227
22194
  init_state2();
22228
22195
  init_audit();
22229
22196
  init_machine_id();
22230
- init_api_url();
22231
22197
  FINDING_TO_SIGNAL3 = {
22232
22198
  dlp: "dlpFindings",
22233
22199
  pii: "piiFindings",
@@ -22242,7 +22208,6 @@ var init_sync = __esm({
22242
22208
  };
22243
22209
  rulesCacheFile = () => path41.join(os36.homedir(), ".node9", "rules-cache.json");
22244
22210
  rulesCacheBackupFile = () => path41.join(os36.homedir(), ".node9", "rules-cache.last-good.json");
22245
- DEFAULT_API_URL2 = `${DEFAULT_API_URL}/policies/sync`;
22246
22211
  DEFAULT_INTERVAL_HOURS = 5;
22247
22212
  MIN_INTERVAL_SECONDS = 15;
22248
22213
  MAX_INTERVAL_SECONDS = 24 * 60 * 60;
@@ -22353,7 +22318,7 @@ function buildWireRows(chunk2) {
22353
22318
  return { rows, consumed: lastNl + 1 };
22354
22319
  }
22355
22320
  function buildBatchEndpoint(rawApiUrl) {
22356
- const validated = validateApiUrl2(rawApiUrl);
22321
+ const validated = validateApiUrl(rawApiUrl);
22357
22322
  if (!validated) return null;
22358
22323
  const base = validated.toString().replace(/\/$/, "").replace(/\/policies\/sync$/, "");
22359
22324
  return `${base}/audit/batch`;
@@ -24318,12 +24283,20 @@ function resolveNode9Binary() {
24318
24283
  function xmlEscape(s) {
24319
24284
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24320
24285
  }
24286
+ function daemonServiceEnv() {
24287
+ const env = { NODE9_AUTO_STARTED: "1" };
24288
+ const allow = process.env[HOST_ALLOW_ENV]?.trim();
24289
+ if (allow && /^[A-Za-z0-9.*,_ -]+$/.test(allow)) env[HOST_ALLOW_ENV] = allow;
24290
+ return env;
24291
+ }
24321
24292
  function launchdPlist(binaryPath) {
24322
24293
  const logDir = path47.join(os42.homedir(), ".node9");
24323
24294
  const nodePath = xmlEscape(process.execPath);
24324
24295
  const scriptPath = xmlEscape(binaryPath);
24325
24296
  const outLog = xmlEscape(path47.join(logDir, "daemon.log"));
24326
24297
  const errLog = xmlEscape(path47.join(logDir, "daemon-error.log"));
24298
+ const envEntries = Object.entries(daemonServiceEnv()).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
24299
+ <string>${xmlEscape(v)}</string>`).join("\n");
24327
24300
  return `<?xml version="1.0" encoding="UTF-8"?>
24328
24301
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
24329
24302
  <plist version="1.0">
@@ -24348,8 +24321,7 @@ function launchdPlist(binaryPath) {
24348
24321
  <string>${errLog}</string>
24349
24322
  <key>EnvironmentVariables</key>
24350
24323
  <dict>
24351
- <key>NODE9_AUTO_STARTED</key>
24352
- <string>1</string>
24324
+ ${envEntries}
24353
24325
  </dict>
24354
24326
  </dict>
24355
24327
  </plist>
@@ -24378,6 +24350,7 @@ function isLaunchdInstalled() {
24378
24350
  return fs48.existsSync(LAUNCHD_PLIST);
24379
24351
  }
24380
24352
  function systemdUnit(binaryPath) {
24353
+ const envLines = Object.entries(daemonServiceEnv()).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
24381
24354
  return `[Unit]
24382
24355
  Description=node9 approval daemon
24383
24356
  After=network.target
@@ -24387,7 +24360,7 @@ Type=simple
24387
24360
  ExecStart=${process.execPath} ${binaryPath} daemon
24388
24361
  Restart=on-failure
24389
24362
  RestartSec=10s
24390
- Environment=NODE9_AUTO_STARTED=1
24363
+ ${envLines}
24391
24364
 
24392
24365
  [Install]
24393
24366
  WantedBy=default.target
@@ -24443,7 +24416,10 @@ function windowsLauncherVbs(nodePath, scriptPath) {
24443
24416
  "' Auto-generated by node9 - starts the approval daemon with no console window.",
24444
24417
  "' Recreated by `node9 daemon install`; removed by `node9 daemon uninstall`.",
24445
24418
  'Set sh = CreateObject("Wscript.Shell")',
24446
- 'sh.Environment("PROCESS")("NODE9_AUTO_STARTED") = "1"',
24419
+ // Values are vetted by daemonServiceEnv: no double quote can reach here.
24420
+ ...Object.entries(daemonServiceEnv()).map(
24421
+ ([k, v]) => `sh.Environment("PROCESS")("${k}") = "${v}"`
24422
+ ),
24447
24423
  `sh.Run """${nodePath}"" ""${scriptPath}"" daemon", 0, False`,
24448
24424
  ""
24449
24425
  ].join("\r\n");
@@ -24650,6 +24626,7 @@ var LAUNCHD_LABEL, LAUNCHD_PLIST, SYSTEMD_UNIT_DIR, SYSTEMD_UNIT, STARTUP_FILE,
24650
24626
  var init_service = __esm({
24651
24627
  "src/daemon/service.ts"() {
24652
24628
  "use strict";
24629
+ init_api_url();
24653
24630
  LAUNCHD_LABEL = "ai.node9.daemon";
24654
24631
  LAUNCHD_PLIST = path47.join(os42.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
24655
24632
  SYSTEMD_UNIT_DIR = path47.join(os42.homedir(), ".config", "systemd", "user");
@@ -52313,12 +52290,18 @@ import * as os44 from "os";
52313
52290
  import chalk11 from "chalk";
52314
52291
 
52315
52292
  // src/auth/cloud-endpoints.ts
52293
+ init_api_url();
52316
52294
  var PROD_BASE = "https://api.node9.ai/api/v1";
52317
52295
  function resolveCloudEndpoint(pathSuffix, override) {
52296
+ const raw = override || process.env.NODE9_API_URL;
52297
+ if (!raw) return PROD_BASE + pathSuffix;
52298
+ if (!validateApiUrl(raw)) {
52299
+ throw new Error(
52300
+ `${override ? "--api-url" : "NODE9_API_URL"} "${raw.slice(0, 200)}" is not an allowed node9 host. For a self-hosted control plane set ${HOST_ALLOW_ENV}=<your-domain>.`
52301
+ );
52302
+ }
52318
52303
  if (override) return override;
52319
- const base = process.env.NODE9_API_URL;
52320
- if (base) return base.replace(/\/intercept\/?$/, "") + pathSuffix;
52321
- return PROD_BASE + pathSuffix;
52304
+ return raw.replace(/\/intercept\/?$/, "") + pathSuffix;
52322
52305
  }
52323
52306
 
52324
52307
  // src/auth/device-login.ts
@@ -52479,14 +52462,27 @@ import * as os45 from "os";
52479
52462
  import * as path49 from "path";
52480
52463
  import chalk12 from "chalk";
52481
52464
  init_safe_text();
52465
+ init_api_url();
52482
52466
  async function revokeSelf(creds) {
52483
- const url = creds.apiUrl.replace(/\/$/, "") + "/machines/self/disconnect";
52467
+ let rejected = null;
52468
+ const base = safeApiUrl(creds.apiUrl, (raw) => {
52469
+ rejected = String(raw).slice(0, 200);
52470
+ }).replace(/\/$/, "");
52471
+ const url = base + "/machines/self/disconnect";
52484
52472
  try {
52485
52473
  const r = await postJson2(url, {}, creds.apiKey);
52486
52474
  return { outcome: "revoked", name: r.name };
52487
52475
  } catch (e) {
52488
52476
  const msg = e instanceof Error ? e.message : String(e);
52489
- if (/HTTP 401/.test(msg)) return { outcome: "already" };
52477
+ if (/HTTP 401/.test(msg)) {
52478
+ if (rejected) {
52479
+ return {
52480
+ outcome: "unreachable",
52481
+ detail: `stored apiUrl ${rejected} is not an allowed host (set ${HOST_ALLOW_ENV} for self-hosted); the revoke went to ${base} and was refused`
52482
+ };
52483
+ }
52484
+ return { outcome: "already" };
52485
+ }
52490
52486
  return { outcome: "unreachable", detail: msg };
52491
52487
  }
52492
52488
  }
@@ -52506,10 +52502,7 @@ function registerLogoutCommand(program2) {
52506
52502
  console.log(chalk12.gray("Not logged in \u2014 nothing to disconnect."));
52507
52503
  return;
52508
52504
  }
52509
- const res = await revokeSelf({
52510
- apiKey: entry.apiKey,
52511
- apiUrl: entry.apiUrl || "https://api.node9.ai/api/v1/intercept"
52512
- });
52505
+ const res = await revokeSelf({ apiKey: entry.apiKey, apiUrl: entry.apiUrl });
52513
52506
  if (res.outcome === "revoked") {
52514
52507
  console.log(
52515
52508
  chalk12.green(
@@ -62635,10 +62628,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
62635
62628
  const prof = JSON.parse(credRaw)[process.env.NODE9_PROFILE || "default"];
62636
62629
  if (prof?.apiKey) {
62637
62630
  console.log(chalk45.bold("Disconnecting from the cloud..."));
62638
- const r = await revokeSelf({
62639
- apiKey: prof.apiKey,
62640
- apiUrl: prof.apiUrl || "https://api.node9.ai/api/v1/intercept"
62641
- });
62631
+ const r = await revokeSelf({ apiKey: prof.apiKey, apiUrl: prof.apiUrl });
62642
62632
  if (r.outcome === "revoked") {
62643
62633
  console.log(chalk45.green(" \u2705 Machine disconnected \u2014 its key is revoked"));
62644
62634
  } else if (r.outcome === "already") {