@node9/proxy 2.22.0 → 2.23.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/index.js CHANGED
@@ -1625,6 +1625,7 @@ function normalizeCommandForPolicyImpl(command) {
1625
1625
  const source = command.slice(s, e);
1626
1626
  if (resolved === source) continue;
1627
1627
  if (resolved === "" || /\s/.test(resolved)) continue;
1628
+ if (/^[;&|()<>]+$/.test(resolved)) continue;
1628
1629
  rewrites.push([s, e, resolved]);
1629
1630
  const quoteOnly = source.replace(/['"]/g, "");
1630
1631
  if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
@@ -2641,6 +2642,60 @@ var NET_BINARIES = /* @__PURE__ */ new Set([
2641
2642
  "rsync"
2642
2643
  ]);
2643
2644
  var VALUE_FLAGS = {
2645
+ // rsync 3.2.7, its own --help: every flag whose operand could be mistaken for
2646
+ // a host. `-e ssh` is the one that matters most (`ssh` is not the destination).
2647
+ rsync: /* @__PURE__ */ new Set([
2648
+ "-e",
2649
+ "--rsh",
2650
+ "-f",
2651
+ "--filter",
2652
+ "-T",
2653
+ "--temp-dir",
2654
+ "-B",
2655
+ "--block-size",
2656
+ "-M",
2657
+ "--remote-option",
2658
+ "--exclude",
2659
+ "--exclude-from",
2660
+ "--include",
2661
+ "--include-from",
2662
+ "--files-from",
2663
+ "--compare-dest",
2664
+ "--copy-dest",
2665
+ "--link-dest",
2666
+ "--partial-dir",
2667
+ "--log-file",
2668
+ "--password-file",
2669
+ "--bwlimit",
2670
+ "--timeout",
2671
+ "--contimeout",
2672
+ "--port",
2673
+ "--sockopts",
2674
+ "--address",
2675
+ "--chmod",
2676
+ "--chown",
2677
+ "--max-size",
2678
+ "--min-size",
2679
+ "--modify-window",
2680
+ "--out-format",
2681
+ "--log-file-format",
2682
+ "--backup-dir",
2683
+ "--suffix",
2684
+ "--iconv",
2685
+ "--max-delete",
2686
+ "--checksum-choice",
2687
+ "--info",
2688
+ "--debug",
2689
+ "--stderr",
2690
+ "--outbuf",
2691
+ "--skip-compress",
2692
+ "--usermap",
2693
+ "--groupmap",
2694
+ "--mkpath",
2695
+ "--write-batch",
2696
+ "--read-batch",
2697
+ "--only-write-batch"
2698
+ ]),
2644
2699
  curl: /* @__PURE__ */ new Set([
2645
2700
  "-d",
2646
2701
  "--data",
@@ -2724,23 +2779,163 @@ var VALUE_FLAGS = {
2724
2779
  ]),
2725
2780
  nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
2726
2781
  };
2782
+ var HOME_VARIABLES = /* @__PURE__ */ new Set(["HOME", "USERPROFILE"]);
2783
+ var assignmentTable = null;
2784
+ var currentStmtOffset = Number.MAX_SAFE_INTEGER;
2785
+ var PAYLOAD_BUDGET = 256;
2786
+ var payloadBudget = 0;
2787
+ var seenPayloads = null;
2788
+ function payloadKey(payload) {
2789
+ if (!assignmentTable || assignmentTable.size === 0) return payload;
2790
+ const bindings = [];
2791
+ for (const [name, rec] of assignmentTable) {
2792
+ if (rec.value !== null) bindings.push(`${name}=${rec.value}`);
2793
+ }
2794
+ return `${payload}\0${bindings.sort().join("")}`;
2795
+ }
2796
+ function claimPayload(payload) {
2797
+ if (!seenPayloads) return "ok";
2798
+ const key = payloadKey(payload);
2799
+ if (seenPayloads.has(key)) return "seen";
2800
+ if (payloadBudget <= 0) return "exhausted";
2801
+ payloadBudget--;
2802
+ seenPayloads.add(key);
2803
+ return "ok";
2804
+ }
2805
+ var UNANALYSABLE_NESTING = {
2806
+ ruleName: "review-unanalysable-nesting",
2807
+ verdict: "review",
2808
+ reason: "This command nests more wrapped shell payloads than the policy engine will unwrap, so some of what it runs was not read.",
2809
+ path: ""
2810
+ };
2811
+ var ASSIGNMENT_HEADS = /* @__PURE__ */ new Set(["export", "declare", "local", "readonly", "typeset"]);
2812
+ function recordTopLevelAssignments(f) {
2813
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
2814
+ for (const stmt of stmts) recordTopLevelStmt(stmt);
2815
+ }
2816
+ function probeBinOp(src) {
2817
+ try {
2818
+ const cmd = syntax.NewParser().Parse(src, "probe")?.Stmts?.[0]?.Cmd;
2819
+ if (cmd && syntax.NodeType(cmd) === "BinaryCmd") return cmd.Op;
2820
+ } catch {
2821
+ }
2822
+ return null;
2823
+ }
2824
+ var AND_OP = probeBinOp("a && b");
2825
+ var OR_OP = probeBinOp("a || b");
2826
+ var AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
2827
+ function recordTopLevelStmt(stmt) {
2828
+ if (!stmt || !stmt.Cmd) return false;
2829
+ const t = syntax.NodeType(stmt.Cmd);
2830
+ if (t === "BinaryCmd") {
2831
+ if (!AND_OR_OPS.has(stmt.Cmd.Op)) return false;
2832
+ if (!recordTopLevelStmt(stmt.Cmd.X)) return false;
2833
+ if (stmt.Cmd.Op === AND_OP) return recordTopLevelStmt(stmt.Cmd.Y);
2834
+ return true;
2835
+ }
2836
+ if (t !== "CallExpr" && t !== "DeclClause") return false;
2837
+ let at = 0;
2838
+ try {
2839
+ at = stmt.Pos().Offset();
2840
+ } catch {
2841
+ at = 0;
2842
+ }
2843
+ recordAssignments(stmt.Cmd, at);
2844
+ if (stmt.Negated) return false;
2845
+ if (t === "DeclClause") return ASSIGNMENT_HEADS.has(stmt.Cmd.Variant?.Value ?? "");
2846
+ return (stmt.Cmd.Args || []).length === 0 && (stmt.Cmd.Assigns || []).length > 0;
2847
+ }
2848
+ function recordAssignments(n, at) {
2849
+ if (!assignmentTable) return;
2850
+ const t = syntax.NodeType(n);
2851
+ let assigns = [];
2852
+ if (t === "CallExpr") {
2853
+ if ((n.Args || []).length > 0) return;
2854
+ assigns = n.Assigns || [];
2855
+ } else if (t === "DeclClause") {
2856
+ if (!ASSIGNMENT_HEADS.has(n.Variant?.Value ?? "")) return;
2857
+ assigns = (n.Args || []).filter((a) => syntax.NodeType(a) === "Assign");
2858
+ } else return;
2859
+ for (const a of assigns) {
2860
+ const name = a?.Name?.Value;
2861
+ if (!name || !a.Value || a.Append) continue;
2862
+ assignmentTable.set(name, { value: resolveWordLiteral(a.Value), at });
2863
+ }
2864
+ }
2865
+ function resolveTrivialSubst(part) {
2866
+ if (syntax.NodeType(part) !== "CmdSubst") return void 0;
2867
+ const stmts = part.Stmts || [];
2868
+ if (stmts.length !== 1) return void 0;
2869
+ const st = stmts[0];
2870
+ if ((st.Redirs || []).length > 0 || st.Negated || st.Background) return void 0;
2871
+ const cmd = st.Cmd;
2872
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return void 0;
2873
+ if ((cmd.Assigns || []).length > 0) return void 0;
2874
+ const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
2875
+ if (words.length === 0 || words.some((w) => w === null)) return void 0;
2876
+ const head = baseWord(words[0]);
2877
+ const rest = words.slice(1);
2878
+ if (head === "echo") {
2879
+ let i = 0;
2880
+ while (i < rest.length && /^-[neE]+$/.test(rest[i])) i++;
2881
+ return rest.slice(i).join(" ");
2882
+ }
2883
+ if (head === "printf") {
2884
+ if (rest.length !== 2) return void 0;
2885
+ if (!/^%s(\\n)?$/.test(rest[0])) return void 0;
2886
+ return rest[1];
2887
+ }
2888
+ return void 0;
2889
+ }
2890
+ function recordedExpansion(name) {
2891
+ if (!assignmentTable || !name) return void 0;
2892
+ const rec = assignmentTable.get(name);
2893
+ if (rec === void 0 || rec.at >= currentStmtOffset) return void 0;
2894
+ return rec.value;
2895
+ }
2896
+ function isPlainParam(p) {
2897
+ if (syntax.NodeType(p) !== "ParamExp") return false;
2898
+ return !(p.Excl || p.Length || p.Width || p.Index || p.Slice || p.Repl || p.Exp);
2899
+ }
2900
+ function expandPlainParam(p) {
2901
+ if (!assignmentTable) return void 0;
2902
+ if (!isPlainParam(p)) return void 0;
2903
+ const recorded = recordedExpansion(p.Param?.Value);
2904
+ if (recorded !== void 0) return recorded;
2905
+ return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
2906
+ }
2727
2907
  function resolveWordLiteral(w) {
2728
2908
  const parts = w?.Parts || [];
2729
2909
  let s = "";
2730
2910
  for (const p of parts) {
2731
- const t = syntax.NodeType(p);
2732
- if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
2733
- else if (t === "SglQuoted") s += p.Value ?? "";
2734
- else if (t === "DblQuoted") {
2735
- const inner = p.Parts || [];
2736
- if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
2737
- s += inner.map((ip) => ip.Value ?? "").join("");
2738
- } else {
2739
- return null;
2740
- }
2911
+ const piece = resolvePart(p, false);
2912
+ if (piece === void 0 || piece === null) return null;
2913
+ s += piece;
2741
2914
  }
2742
2915
  return s;
2743
2916
  }
2917
+ function resolvePart(p, inQuotes) {
2918
+ const t = syntax.NodeType(p);
2919
+ if (t === "Lit") {
2920
+ const raw = p.Value ?? "";
2921
+ if (!inQuotes) return raw.replace(/\\(.)/g, "$1");
2922
+ return assignmentTable ? raw.replace(/\\([$`"\\])/g, "$1") : raw;
2923
+ }
2924
+ if (t === "SglQuoted") return p.Value ?? "";
2925
+ if (t === "ParamExp") return expandPlainParam(p);
2926
+ if (t === "CmdSubst") return assignmentTable ? resolveTrivialSubst(p) : void 0;
2927
+ if (t === "DblQuoted" && !inQuotes) {
2928
+ const inner = p.Parts || [];
2929
+ let out = "";
2930
+ for (const ip of inner) {
2931
+ const piece = resolvePart(ip, true);
2932
+ if (piece === void 0 || piece === null) return piece;
2933
+ out += piece;
2934
+ }
2935
+ return out;
2936
+ }
2937
+ return void 0;
2938
+ }
2744
2939
  function parseDestHost(token) {
2745
2940
  if (!token) return null;
2746
2941
  let t = token.trim();
@@ -2795,6 +2990,7 @@ function destTokensForBinary(binary, args) {
2795
2990
  case "ssh":
2796
2991
  return positionals.slice(0, 1);
2797
2992
  case "scp":
2993
+ case "rsync":
2798
2994
  return positionals.filter((p) => p.includes(":") || p.includes("@"));
2799
2995
  case "nc":
2800
2996
  case "ncat":
@@ -2995,17 +3191,35 @@ function analyzeFsOperationImpl(command, depth = 0) {
2995
3191
  const f = parseShared(command);
2996
3192
  if (f === PARSE_FAIL) return null;
2997
3193
  let result = null;
3194
+ const outerTable = assignmentTable;
3195
+ const outerOffset = currentStmtOffset;
3196
+ const outerSeen = seenPayloads;
3197
+ const outerBudget = payloadBudget;
3198
+ if (depth === 0) {
3199
+ seenPayloads = /* @__PURE__ */ new Set();
3200
+ payloadBudget = PAYLOAD_BUDGET;
3201
+ }
3202
+ assignmentTable = new Map(
3203
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
3204
+ );
3205
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
2998
3206
  try {
3207
+ recordTopLevelAssignments(f);
2999
3208
  syntax.Walk(f, (node) => {
3000
3209
  if (!node || result?.verdict === "block") return false;
3001
3210
  const n = node;
3002
3211
  const nodeType = syntax.NodeType(n);
3003
3212
  if (nodeType === "Stmt") {
3213
+ try {
3214
+ currentStmtOffset = n.Pos().Offset();
3215
+ } catch {
3216
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
3217
+ }
3004
3218
  result = stricter(result, jailedRedirectRead(n));
3005
3219
  return result?.verdict !== "block";
3006
3220
  }
3007
3221
  if (nodeType !== "CallExpr") return true;
3008
- const { name, flags, paths, words, args } = extractLiteralArgs(n);
3222
+ const { name, flags, paths, words } = extractLiteralArgs(n);
3009
3223
  if (!name) return true;
3010
3224
  if (name === "rm") {
3011
3225
  const flagStr = flags.join("").toLowerCase();
@@ -3034,9 +3248,30 @@ function analyzeFsOperationImpl(command, depth = 0) {
3034
3248
  }
3035
3249
  }
3036
3250
  }
3037
- if (depth < 1) {
3251
+ if (depth < 24 && name === "find") {
3252
+ for (const action of findActions(words, 0)) {
3253
+ const h = unwrapCommandHead(action);
3254
+ const inner = literalShellPayload(action.slice(h), baseWord(action[h]));
3255
+ if (inner === null) continue;
3256
+ const claim = claimPayload(inner);
3257
+ if (claim === "exhausted") {
3258
+ result = stricter(result, UNANALYSABLE_NESTING);
3259
+ continue;
3260
+ }
3261
+ if (claim === "seen") continue;
3262
+ const v = analyzeFsOperationImpl(inner, depth + 1);
3263
+ result = stricter(result, v);
3264
+ if (result?.verdict === "block") return false;
3265
+ }
3266
+ }
3267
+ if (depth < 24) {
3038
3268
  const payload = literalShellPayload(words, name);
3039
- if (payload !== null) {
3269
+ const claim = payload === null ? "seen" : claimPayload(payload);
3270
+ if (claim === "exhausted") {
3271
+ result = stricter(result, UNANALYSABLE_NESTING);
3272
+ return true;
3273
+ }
3274
+ if (payload !== null && claim === "ok") {
3040
3275
  const inner = analyzeFsOperationImpl(payload, depth + 1);
3041
3276
  if (inner) {
3042
3277
  result = inner;
@@ -3045,7 +3280,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
3045
3280
  return true;
3046
3281
  }
3047
3282
  }
3048
- const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
3283
+ const readPaths = FS_READ_TOOLS.has(name) ? readerPaths(words, 0) : wrappedReadPaths(words, name);
3049
3284
  if (readPaths) {
3050
3285
  for (const p of readPaths) {
3051
3286
  result = stricter(result, matchSensitivePath2(p));
@@ -3060,6 +3295,11 @@ function analyzeFsOperationImpl(command, depth = 0) {
3060
3295
  return result;
3061
3296
  } catch {
3062
3297
  return null;
3298
+ } finally {
3299
+ assignmentTable = outerTable;
3300
+ currentStmtOffset = outerOffset;
3301
+ seenPayloads = outerSeen;
3302
+ if (depth === 0) payloadBudget = outerBudget;
3063
3303
  }
3064
3304
  }
3065
3305
  function stricter(a, b) {
@@ -3117,6 +3357,18 @@ function resolveCopyShape(words, h) {
3117
3357
  return null;
3118
3358
  }
3119
3359
  var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
3360
+ function findAction(words, k) {
3361
+ const end = words.findIndex((w, i) => i > k && (w === ";" || w === "+"));
3362
+ return words.slice(k + 1, end < 0 ? words.length : end);
3363
+ }
3364
+ function findActions(words, h) {
3365
+ const out = [];
3366
+ for (let i = h + 1; i < words.length; i++) {
3367
+ const w = words[i];
3368
+ if (w !== null && FIND_EXEC_FLAGS.has(w)) out.push(findAction(words, i));
3369
+ }
3370
+ return out;
3371
+ }
3120
3372
  function findStartPoints(words, h) {
3121
3373
  const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
3122
3374
  if (k < 0) return { k, starts: [] };
@@ -3132,8 +3384,13 @@ function copySourcePaths(words) {
3132
3384
  if (fi >= 0) {
3133
3385
  const { k, starts } = findStartPoints(words, fi);
3134
3386
  if (k < 0) return [];
3135
- const action = unwrapCommandHead(words.slice(k + 1));
3136
- return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
3387
+ const out = [];
3388
+ for (const action of findActions(words, fi)) {
3389
+ const h2 = unwrapCommandHead(action);
3390
+ if (!resolveCopyShape(action, h2)) continue;
3391
+ out.push(...starts, ...copySourcePaths(action));
3392
+ }
3393
+ return out;
3137
3394
  }
3138
3395
  if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
3139
3396
  const r = resolveCopyShape(words, h);
@@ -3320,6 +3577,15 @@ function flagEffect(token, shape, known) {
3320
3577
  }
3321
3578
  return NONE;
3322
3579
  }
3580
+ function readerPaths(words, h) {
3581
+ const head = baseWord(words[h]);
3582
+ const from = h + 1;
3583
+ const flags = words.slice(from).filter((w) => w !== null && w.startsWith("-"));
3584
+ return [
3585
+ ...readTargets(head, positionedArgs(words, from), flags, words, from),
3586
+ ...flagOperandFiles(head, words, from)
3587
+ ];
3588
+ }
3323
3589
  function readTargets(verb, args, flags, words = [], from = 1) {
3324
3590
  const shape = PATTERN_VERBS[verb];
3325
3591
  if (!shape) return args.map((a) => a.value);
@@ -3401,18 +3667,21 @@ function flagOperandFiles(verb, words, from) {
3401
3667
  function wrappedReadPaths(words, name) {
3402
3668
  if (name === "find") {
3403
3669
  const { k, starts } = findStartPoints(words, 0);
3404
- return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
3670
+ if (k < 0) return null;
3671
+ const paths = [];
3672
+ let reads = false;
3673
+ for (const action of findActions(words, 0)) {
3674
+ const h2 = unwrapCommandHead(action);
3675
+ if (!isReaderWord(action[h2] ?? null)) continue;
3676
+ reads = true;
3677
+ paths.push(...readerPaths(action, h2));
3678
+ }
3679
+ return reads ? [...starts, ...paths] : paths;
3405
3680
  }
3406
3681
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
3407
3682
  const h = unwrapCommandHead(words);
3408
3683
  if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
3409
- const head = baseWord(words[h]);
3410
- const rest = words.slice(h + 1);
3411
- const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
3412
- return [
3413
- ...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
3414
- ...flagOperandFiles(head, words, h + 1)
3415
- ];
3684
+ return readerPaths(words, h);
3416
3685
  }
3417
3686
  function literalShellPayload(words, name) {
3418
3687
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;