@node9/proxy 2.9.3 → 2.10.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 CHANGED
@@ -2046,6 +2046,12 @@ function normalizeIpLiteral(host) {
2046
2046
  return null;
2047
2047
  }
2048
2048
  }
2049
+ function isStrictGatedTier(tier) {
2050
+ return STRICT_TIERS.has(tier);
2051
+ }
2052
+ function ssrfReason(m, asWritten) {
2053
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2054
+ }
2049
2055
  function classifySsrf(host) {
2050
2056
  try {
2051
2057
  if (typeof host !== "string" || !host) return null;
@@ -2091,17 +2097,66 @@ function ssrfFloor(tokens, opts = {}) {
2091
2097
  for (const { token, binary } of tokens) {
2092
2098
  const m = classifySsrf(token);
2093
2099
  if (!m) continue;
2094
- if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
2100
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2095
2101
  if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2096
- return {
2097
- ...m,
2098
- host: token,
2099
- binary,
2100
- reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
2101
- };
2102
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2102
2103
  }
2103
2104
  return null;
2104
2105
  }
2106
+ function bareToolName(toolName) {
2107
+ const parts = toolName.split("__");
2108
+ return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
2109
+ }
2110
+ function valuesAt(args, path77) {
2111
+ let cursors = [args];
2112
+ for (const rawSegment of path77.split(".")) {
2113
+ const isArray = rawSegment.endsWith("[]");
2114
+ const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
2115
+ const next = [];
2116
+ for (const cursor of cursors) {
2117
+ if (cursor === null || typeof cursor !== "object") continue;
2118
+ const child = cursor[key];
2119
+ if (isArray) {
2120
+ if (Array.isArray(child)) next.push(...child);
2121
+ } else if (child !== void 0) {
2122
+ next.push(child);
2123
+ }
2124
+ }
2125
+ cursors = next;
2126
+ if (cursors.length === 0) return [];
2127
+ }
2128
+ return cursors.filter((v) => typeof v === "string");
2129
+ }
2130
+ function hostOf(value) {
2131
+ try {
2132
+ const u = new URL(value);
2133
+ const h = u.hostname;
2134
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
2135
+ } catch {
2136
+ return null;
2137
+ }
2138
+ }
2139
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
2140
+ try {
2141
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
2142
+ if (!paths) return null;
2143
+ const exempt2 = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
2144
+ for (const path77 of paths) {
2145
+ for (const value of valuesAt(args, path77)) {
2146
+ const host = hostOf(value);
2147
+ if (!host) continue;
2148
+ const m = classifySsrf(host);
2149
+ if (!m) continue;
2150
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2151
+ if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2152
+ return { ...m, argPath: path77, host, reason: ssrfReason(m, host) };
2153
+ }
2154
+ }
2155
+ return null;
2156
+ } catch {
2157
+ return null;
2158
+ }
2159
+ }
2105
2160
  function resolveCheck(v) {
2106
2161
  return v === "off" || v === "block" ? v : "review";
2107
2162
  }
@@ -2199,6 +2254,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2199
2254
  };
2200
2255
  }
2201
2256
  }
2257
+ {
2258
+ const dest = ssrfDestinationFloor(toolName, args, {
2259
+ ssrfAllow: config.policy.egress?.ssrfAllow,
2260
+ ssrfStrict: config.policy.egress?.ssrfStrict
2261
+ });
2262
+ if (dest) {
2263
+ return {
2264
+ decision: "block",
2265
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
2266
+ reason: dest.reason,
2267
+ ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
2268
+ ruleDescription: dest.reason,
2269
+ tier: 3
2270
+ };
2271
+ }
2272
+ }
2202
2273
  if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2203
2274
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2204
2275
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
@@ -3205,7 +3276,7 @@ function* stringValues(obj, depth = 0) {
3205
3276
  }
3206
3277
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3207
3278
  }
3208
- 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, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, 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, VERDICT_RANK, SQL_DML_KEYWORDS, 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, 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, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3279
+ 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, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, 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, 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, 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, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3209
3280
  var init_dist = __esm({
3210
3281
  "packages/policy-engine/dist/index.mjs"() {
3211
3282
  "use strict";
@@ -4382,6 +4453,13 @@ var init_dist = __esm({
4382
4453
  cgnat: "a carrier-grade NAT address",
4383
4454
  private: "a loopback or private address"
4384
4455
  };
4456
+ DESTINATION_ARGS = /* @__PURE__ */ new Map([
4457
+ ["webfetch", ["url"]],
4458
+ ["fetch", ["url", "uri"]],
4459
+ ["navigate", ["url"]],
4460
+ ["preview_start", ["url"]],
4461
+ ["browser_batch", ["actions[].input.url"]]
4462
+ ]);
4385
4463
  VERDICT_RANK = {
4386
4464
  allow: 0,
4387
4465
  review: 1,
@@ -8752,6 +8830,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8752
8830
  };
8753
8831
  }
8754
8832
  }
8833
+ {
8834
+ const dest = ssrfDestinationFloor(toolName, args, {
8835
+ ssrfAllow: config.policy.egress?.ssrfAllow,
8836
+ ssrfStrict: config.policy.egress?.ssrfStrict
8837
+ });
8838
+ if (dest && !isObserveMode) {
8839
+ if (!isManual)
8840
+ appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
8841
+ return {
8842
+ approved: false,
8843
+ checkedBy: "local-policy",
8844
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
8845
+ reason: dest.reason,
8846
+ // AuthResult carries no ruleName; the rule identity the audit row needs
8847
+ // travels in ruleHit, as it does for a smart-rule block.
8848
+ ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
8849
+ ruleDescription: dest.reason
8850
+ };
8851
+ }
8852
+ }
8755
8853
  if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
8756
8854
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
8757
8855
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
@@ -9427,6 +9525,7 @@ var init_orchestrator = __esm({
9427
9525
  init_dlp();
9428
9526
  init_registry();
9429
9527
  init_dist();
9528
+ init_dist();
9430
9529
  init_audit();
9431
9530
  init_config();
9432
9531
  init_policy();
package/dist/cli.mjs CHANGED
@@ -2057,6 +2057,12 @@ function normalizeIpLiteral(host) {
2057
2057
  return null;
2058
2058
  }
2059
2059
  }
2060
+ function isStrictGatedTier(tier) {
2061
+ return STRICT_TIERS.has(tier);
2062
+ }
2063
+ function ssrfReason(m, asWritten) {
2064
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2065
+ }
2060
2066
  function classifySsrf(host) {
2061
2067
  try {
2062
2068
  if (typeof host !== "string" || !host) return null;
@@ -2102,17 +2108,66 @@ function ssrfFloor(tokens, opts = {}) {
2102
2108
  for (const { token, binary } of tokens) {
2103
2109
  const m = classifySsrf(token);
2104
2110
  if (!m) continue;
2105
- if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
2111
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2106
2112
  if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2107
- return {
2108
- ...m,
2109
- host: token,
2110
- binary,
2111
- reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
2112
- };
2113
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2113
2114
  }
2114
2115
  return null;
2115
2116
  }
2117
+ function bareToolName(toolName) {
2118
+ const parts = toolName.split("__");
2119
+ return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
2120
+ }
2121
+ function valuesAt(args, path77) {
2122
+ let cursors = [args];
2123
+ for (const rawSegment of path77.split(".")) {
2124
+ const isArray = rawSegment.endsWith("[]");
2125
+ const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
2126
+ const next = [];
2127
+ for (const cursor of cursors) {
2128
+ if (cursor === null || typeof cursor !== "object") continue;
2129
+ const child = cursor[key];
2130
+ if (isArray) {
2131
+ if (Array.isArray(child)) next.push(...child);
2132
+ } else if (child !== void 0) {
2133
+ next.push(child);
2134
+ }
2135
+ }
2136
+ cursors = next;
2137
+ if (cursors.length === 0) return [];
2138
+ }
2139
+ return cursors.filter((v) => typeof v === "string");
2140
+ }
2141
+ function hostOf(value) {
2142
+ try {
2143
+ const u = new URL(value);
2144
+ const h = u.hostname;
2145
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
2146
+ } catch {
2147
+ return null;
2148
+ }
2149
+ }
2150
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
2151
+ try {
2152
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
2153
+ if (!paths) return null;
2154
+ const exempt2 = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
2155
+ for (const path77 of paths) {
2156
+ for (const value of valuesAt(args, path77)) {
2157
+ const host = hostOf(value);
2158
+ if (!host) continue;
2159
+ const m = classifySsrf(host);
2160
+ if (!m) continue;
2161
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2162
+ if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2163
+ return { ...m, argPath: path77, host, reason: ssrfReason(m, host) };
2164
+ }
2165
+ }
2166
+ return null;
2167
+ } catch {
2168
+ return null;
2169
+ }
2170
+ }
2116
2171
  function resolveCheck(v) {
2117
2172
  return v === "off" || v === "block" ? v : "review";
2118
2173
  }
@@ -2210,6 +2265,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2210
2265
  };
2211
2266
  }
2212
2267
  }
2268
+ {
2269
+ const dest = ssrfDestinationFloor(toolName, args, {
2270
+ ssrfAllow: config.policy.egress?.ssrfAllow,
2271
+ ssrfStrict: config.policy.egress?.ssrfStrict
2272
+ });
2273
+ if (dest) {
2274
+ return {
2275
+ decision: "block",
2276
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
2277
+ reason: dest.reason,
2278
+ ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
2279
+ ruleDescription: dest.reason,
2280
+ tier: 3
2281
+ };
2282
+ }
2283
+ }
2213
2284
  if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2214
2285
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2215
2286
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
@@ -3216,7 +3287,7 @@ function* stringValues(obj, depth = 0) {
3216
3287
  }
3217
3288
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3218
3289
  }
3219
- 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, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, 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, VERDICT_RANK, SQL_DML_KEYWORDS, 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, 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, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3290
+ 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, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, 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, 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, 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, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3220
3291
  var init_dist = __esm({
3221
3292
  "packages/policy-engine/dist/index.mjs"() {
3222
3293
  "use strict";
@@ -4386,6 +4457,13 @@ var init_dist = __esm({
4386
4457
  cgnat: "a carrier-grade NAT address",
4387
4458
  private: "a loopback or private address"
4388
4459
  };
4460
+ DESTINATION_ARGS = /* @__PURE__ */ new Map([
4461
+ ["webfetch", ["url"]],
4462
+ ["fetch", ["url", "uri"]],
4463
+ ["navigate", ["url"]],
4464
+ ["preview_start", ["url"]],
4465
+ ["browser_batch", ["actions[].input.url"]]
4466
+ ]);
4389
4467
  VERDICT_RANK = {
4390
4468
  allow: 0,
4391
4469
  review: 1,
@@ -8755,6 +8833,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8755
8833
  };
8756
8834
  }
8757
8835
  }
8836
+ {
8837
+ const dest = ssrfDestinationFloor(toolName, args, {
8838
+ ssrfAllow: config.policy.egress?.ssrfAllow,
8839
+ ssrfStrict: config.policy.egress?.ssrfStrict
8840
+ });
8841
+ if (dest && !isObserveMode) {
8842
+ if (!isManual)
8843
+ appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
8844
+ return {
8845
+ approved: false,
8846
+ checkedBy: "local-policy",
8847
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
8848
+ reason: dest.reason,
8849
+ // AuthResult carries no ruleName; the rule identity the audit row needs
8850
+ // travels in ruleHit, as it does for a smart-rule block.
8851
+ ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
8852
+ ruleDescription: dest.reason
8853
+ };
8854
+ }
8855
+ }
8758
8856
  if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
8759
8857
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
8760
8858
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
@@ -9429,6 +9527,7 @@ var init_orchestrator = __esm({
9429
9527
  init_dlp();
9430
9528
  init_registry();
9431
9529
  init_dist();
9530
+ init_dist();
9432
9531
  init_audit();
9433
9532
  init_config();
9434
9533
  init_policy();
package/dist/index.js CHANGED
@@ -3126,6 +3126,12 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
3126
3126
  "100.100.100.200"
3127
3127
  ]);
3128
3128
  var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
3129
+ function isStrictGatedTier(tier) {
3130
+ return STRICT_TIERS.has(tier);
3131
+ }
3132
+ function ssrfReason(m, asWritten) {
3133
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
3134
+ }
3129
3135
  var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
3130
3136
  var v4Octets = (a) => {
3131
3137
  const p = a.split(".");
@@ -3184,17 +3190,73 @@ function ssrfFloor(tokens, opts = {}) {
3184
3190
  for (const { token, binary } of tokens) {
3185
3191
  const m = classifySsrf(token);
3186
3192
  if (!m) continue;
3187
- if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
3193
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3188
3194
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3189
- return {
3190
- ...m,
3191
- host: token,
3192
- binary,
3193
- reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
3194
- };
3195
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
3195
3196
  }
3196
3197
  return null;
3197
3198
  }
3199
+ var DESTINATION_ARGS = /* @__PURE__ */ new Map([
3200
+ ["webfetch", ["url"]],
3201
+ ["fetch", ["url", "uri"]],
3202
+ ["navigate", ["url"]],
3203
+ ["preview_start", ["url"]],
3204
+ ["browser_batch", ["actions[].input.url"]]
3205
+ ]);
3206
+ function bareToolName(toolName) {
3207
+ const parts = toolName.split("__");
3208
+ return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
3209
+ }
3210
+ function valuesAt(args, path15) {
3211
+ let cursors = [args];
3212
+ for (const rawSegment of path15.split(".")) {
3213
+ const isArray = rawSegment.endsWith("[]");
3214
+ const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
3215
+ const next = [];
3216
+ for (const cursor of cursors) {
3217
+ if (cursor === null || typeof cursor !== "object") continue;
3218
+ const child = cursor[key];
3219
+ if (isArray) {
3220
+ if (Array.isArray(child)) next.push(...child);
3221
+ } else if (child !== void 0) {
3222
+ next.push(child);
3223
+ }
3224
+ }
3225
+ cursors = next;
3226
+ if (cursors.length === 0) return [];
3227
+ }
3228
+ return cursors.filter((v) => typeof v === "string");
3229
+ }
3230
+ function hostOf(value) {
3231
+ try {
3232
+ const u = new URL(value);
3233
+ const h = u.hostname;
3234
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
3235
+ } catch {
3236
+ return null;
3237
+ }
3238
+ }
3239
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
3240
+ try {
3241
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3242
+ if (!paths) return null;
3243
+ const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
3244
+ for (const path15 of paths) {
3245
+ for (const value of valuesAt(args, path15)) {
3246
+ const host = hostOf(value);
3247
+ if (!host) continue;
3248
+ const m = classifySsrf(host);
3249
+ if (!m) continue;
3250
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3251
+ if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3252
+ return { ...m, argPath: path15, host, reason: ssrfReason(m, host) };
3253
+ }
3254
+ }
3255
+ return null;
3256
+ } catch {
3257
+ return null;
3258
+ }
3259
+ }
3198
3260
  function resolveCheck(v) {
3199
3261
  return v === "off" || v === "block" ? v : "review";
3200
3262
  }
@@ -3298,6 +3360,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3298
3360
  };
3299
3361
  }
3300
3362
  }
3363
+ {
3364
+ const dest = ssrfDestinationFloor(toolName, args, {
3365
+ ssrfAllow: config.policy.egress?.ssrfAllow,
3366
+ ssrfStrict: config.policy.egress?.ssrfStrict
3367
+ });
3368
+ if (dest) {
3369
+ return {
3370
+ decision: "block",
3371
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
3372
+ reason: dest.reason,
3373
+ ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
3374
+ ruleDescription: dest.reason,
3375
+ tier: 3
3376
+ };
3377
+ }
3378
+ }
3301
3379
  if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
3302
3380
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3303
3381
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
@@ -7170,6 +7248,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7170
7248
  };
7171
7249
  }
7172
7250
  }
7251
+ {
7252
+ const dest = ssrfDestinationFloor(toolName, args, {
7253
+ ssrfAllow: config.policy.egress?.ssrfAllow,
7254
+ ssrfStrict: config.policy.egress?.ssrfStrict
7255
+ });
7256
+ if (dest && !isObserveMode) {
7257
+ if (!isManual)
7258
+ appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
7259
+ return {
7260
+ approved: false,
7261
+ checkedBy: "local-policy",
7262
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
7263
+ reason: dest.reason,
7264
+ // AuthResult carries no ruleName; the rule identity the audit row needs
7265
+ // travels in ruleHit, as it does for a smart-rule block.
7266
+ ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
7267
+ ruleDescription: dest.reason
7268
+ };
7269
+ }
7270
+ }
7173
7271
  if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
7174
7272
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
7175
7273
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
package/dist/index.mjs CHANGED
@@ -3096,6 +3096,12 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
3096
3096
  "100.100.100.200"
3097
3097
  ]);
3098
3098
  var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
3099
+ function isStrictGatedTier(tier) {
3100
+ return STRICT_TIERS.has(tier);
3101
+ }
3102
+ function ssrfReason(m, asWritten) {
3103
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
3104
+ }
3099
3105
  var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
3100
3106
  var v4Octets = (a) => {
3101
3107
  const p = a.split(".");
@@ -3154,17 +3160,73 @@ function ssrfFloor(tokens, opts = {}) {
3154
3160
  for (const { token, binary } of tokens) {
3155
3161
  const m = classifySsrf(token);
3156
3162
  if (!m) continue;
3157
- if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
3163
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3158
3164
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3159
- return {
3160
- ...m,
3161
- host: token,
3162
- binary,
3163
- reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
3164
- };
3165
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
3165
3166
  }
3166
3167
  return null;
3167
3168
  }
3169
+ var DESTINATION_ARGS = /* @__PURE__ */ new Map([
3170
+ ["webfetch", ["url"]],
3171
+ ["fetch", ["url", "uri"]],
3172
+ ["navigate", ["url"]],
3173
+ ["preview_start", ["url"]],
3174
+ ["browser_batch", ["actions[].input.url"]]
3175
+ ]);
3176
+ function bareToolName(toolName) {
3177
+ const parts = toolName.split("__");
3178
+ return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
3179
+ }
3180
+ function valuesAt(args, path15) {
3181
+ let cursors = [args];
3182
+ for (const rawSegment of path15.split(".")) {
3183
+ const isArray = rawSegment.endsWith("[]");
3184
+ const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
3185
+ const next = [];
3186
+ for (const cursor of cursors) {
3187
+ if (cursor === null || typeof cursor !== "object") continue;
3188
+ const child = cursor[key];
3189
+ if (isArray) {
3190
+ if (Array.isArray(child)) next.push(...child);
3191
+ } else if (child !== void 0) {
3192
+ next.push(child);
3193
+ }
3194
+ }
3195
+ cursors = next;
3196
+ if (cursors.length === 0) return [];
3197
+ }
3198
+ return cursors.filter((v) => typeof v === "string");
3199
+ }
3200
+ function hostOf(value) {
3201
+ try {
3202
+ const u = new URL(value);
3203
+ const h = u.hostname;
3204
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
3205
+ } catch {
3206
+ return null;
3207
+ }
3208
+ }
3209
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
3210
+ try {
3211
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
3212
+ if (!paths) return null;
3213
+ const exempt = new Set((opts.ssrfAllow ?? []).map((a) => classifySsrf(a)?.normalized ?? a));
3214
+ for (const path15 of paths) {
3215
+ for (const value of valuesAt(args, path15)) {
3216
+ const host = hostOf(value);
3217
+ if (!host) continue;
3218
+ const m = classifySsrf(host);
3219
+ if (!m) continue;
3220
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
3221
+ if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3222
+ return { ...m, argPath: path15, host, reason: ssrfReason(m, host) };
3223
+ }
3224
+ }
3225
+ return null;
3226
+ } catch {
3227
+ return null;
3228
+ }
3229
+ }
3168
3230
  function resolveCheck(v) {
3169
3231
  return v === "off" || v === "block" ? v : "review";
3170
3232
  }
@@ -3268,6 +3330,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3268
3330
  };
3269
3331
  }
3270
3332
  }
3333
+ {
3334
+ const dest = ssrfDestinationFloor(toolName, args, {
3335
+ ssrfAllow: config.policy.egress?.ssrfAllow,
3336
+ ssrfStrict: config.policy.egress?.ssrfStrict
3337
+ });
3338
+ if (dest) {
3339
+ return {
3340
+ decision: "block",
3341
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
3342
+ reason: dest.reason,
3343
+ ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
3344
+ ruleDescription: dest.reason,
3345
+ tier: 3
3346
+ };
3347
+ }
3348
+ }
3271
3349
  if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
3272
3350
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3273
3351
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
@@ -7140,6 +7218,26 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
7140
7218
  };
7141
7219
  }
7142
7220
  }
7221
+ {
7222
+ const dest = ssrfDestinationFloor(toolName, args, {
7223
+ ssrfAllow: config.policy.egress?.ssrfAllow,
7224
+ ssrfStrict: config.policy.egress?.ssrfStrict
7225
+ });
7226
+ if (dest && !isObserveMode) {
7227
+ if (!isManual)
7228
+ appendLocalAudit(toolName, args, "deny", "ssrf-destination", meta, hashAuditArgs);
7229
+ return {
7230
+ approved: false,
7231
+ checkedBy: "local-policy",
7232
+ blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
7233
+ reason: dest.reason,
7234
+ // AuthResult carries no ruleName; the rule identity the audit row needs
7235
+ // travels in ruleHit, as it does for a smart-rule block.
7236
+ ruleHit: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
7237
+ ruleDescription: dest.reason
7238
+ };
7239
+ }
7240
+ }
7143
7241
  if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
7144
7242
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
7145
7243
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.9.3",
3
+ "version": "2.10.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",