@zicolasjac-ai/remote-readonly-ssh 1.1.2 → 1.1.3

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/README.md CHANGED
@@ -108,11 +108,20 @@ npm test # 运行单元测试(node --test)
108
108
  ## 安全模型
109
109
 
110
110
  - **双层校验**:每条命令必须以允许前缀开头,且不得含禁止模式(rm/mv/vi/systemctl/sudo/重定向到文件/shadow/ssh 密钥等),否则一律拒绝。
111
- - **防绕过加固**:路径 `..` 穿越规范化、反斜杠转义、命令替换(`$()`、反引号)、分隔符(`;` `&` `&&`)等攻击面全部封堵。
111
+ - **管道分段校验**:命令按 `|` 分段(引号内的 `|` 不分割),每个管道段都必须独立通过白名单——`... | sh`、`... | base64 -d` 等尾段注入全部拒绝。
112
+ - **重定向白名单化**:仅放行 `2>&1` 形式的流重定向,`>`、`>>`、`>|`(强制覆盖)、`>&file` 等任何写文件形态一律拒绝。
113
+ - **路径黑名单全局生效**:`exec-only` / `diff` / `find` 命令中出现的所有绝对路径(含规范化后的 `//x`、`/./x`、`/a/../b` 形态)都与 `read-file`/`dir` 受同一份 `blocked_paths` 约束。
114
+ - **防绕过加固**:路径 `..` 穿越规范化、通配符 glob(`/etc/*/shadow`)、反斜杠转义、命令替换(`$()`、反引号)、变量展开(`$HOME`)、波浪号(`~`)、分隔符(`;` `&` `&&`)、换行注入等攻击面全部封堵;`hostname <name>`、`date -s` 等状态变更命令拒绝。
112
115
  - **参数防注入**:`proc`/`ports`/`bin-version` 参数做严格字符集校验。
113
116
  - **主机白名单**:未登记 IP 一律拒绝。
114
117
  - **无写原语**:工具集中不存在任何写操作路径;写操作由人工执行,本工具只读。
115
118
 
119
+ **已知边界**(设计取舍):
120
+
121
+ - `--config` / 环境变量 `RRS_CONFIG` 可指向任意配置文件——本工具防护的是"AI 误发危险命令",不防护对本地配置的恶意篡改(本地已有写权限的攻击者不在威胁模型内)。
122
+ - `exec-only` 的相对路径参数(如 `cat file.txt`)不经过路径白名单(无法可靠区分文件名与普通参数);家目录敏感文件已由禁词表(`.ssh`、`id_rsa`、`history` 等)与 `blocked_paths` 尽可能覆盖。
123
+ - `exec-only` 中 `$`(含 grep 行尾锚 `'$'`)被整体禁用——需要正则锚的场景请用 `proc` 工具。
124
+
116
125
  ## 许可证
117
126
 
118
127
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zicolasjac-ai/remote-readonly-ssh",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "只读 SSH 运维 CLI 与 Agent Skill:通过 JumpServer 堡垒机白名单强制执行只读查询;附 SKILL.md 让 opencode / Claude Code / Cursor 等 AI 助手自动发现并正确调用。",
5
5
  "license": "MIT",
6
6
  "bin": {
package/src/whitelist.js CHANGED
@@ -18,35 +18,80 @@ const FORBIDDEN_PATTERNS = [
18
18
  /history/,
19
19
  // ---- anti-bypass hardening ----
20
20
  /\\/, // backslash escapes (r\m == rm)
21
- /\$\(/, // command substitution
22
21
  /`/, // backtick substitution
23
- /\$\{/, // parameter expansion (blocked to be safe)
24
22
  /;/, // command separator
25
23
  /(?<![>])&|&(?![0-9&])/, // single & / && (allows only >&N redirect form)
26
24
  /\|\|/, // or operator
27
25
  /[\r\n]/, // embedded newline separators
26
+ /~/, // tilde expansion (== /root for root user)
27
+ /\$/, // variable expansion ($HOME etc; covers $( and ${ too)
28
+ /^hostname\s+\S/, // hostname <name> sets the system hostname
29
+ /^date\s+(-s|--set)/, // date --set changes the system clock
28
30
  ];
29
31
 
30
- const ARG_RE = /^[A-Za-z0-9._\-/:*]+$/;
32
+ // 引号感知的管道分段:单双引号内的 | 不分割(grep -E 'a|b' 合法)
33
+ function splitPipeline(cmd) {
34
+ const segs = [];
35
+ let cur = "";
36
+ let sq = false;
37
+ let dq = false;
38
+ for (const ch of cmd) {
39
+ if (ch === "'" && !dq) sq = !sq;
40
+ else if (ch === '"' && !sq) dq = !dq;
41
+ if (ch === "|" && !sq && !dq) { segs.push(cur); cur = ""; }
42
+ else cur += ch;
43
+ }
44
+ segs.push(cur);
45
+ return segs.map((s) => s.trim()).filter(Boolean);
46
+ }
47
+
48
+ // 提取命令中所有绝对路径 token(供 blocked_paths 黑名单核查)
49
+ function extractAbsPaths(cmd) {
50
+ const out = [];
51
+ for (const m of cmd.matchAll(/(?:^|[\s'"])(\/[A-Za-z0-9._\-/]+)/g)) {
52
+ out.push(path.posix.normalize(m[1]));
53
+ }
54
+ return out;
55
+ }
31
56
 
32
57
  function validateCmd(cmd, conf) {
33
58
  const c = String(cmd).trim();
34
59
  if (!c) throw new Error("空命令");
35
60
  if (/^(sudo |su )/.test(c)) throw new Error(`拒绝: 不允许提权命令 [${c}]`);
61
+ // 管道分段校验:每个管道段都必须独立通过白名单(防 "| sh"、"| base64 -d" 等尾段注入)
36
62
  const prefixes = conf.allowed_command_prefixes || [];
37
63
  const patterns = conf.allowed_command_patterns || [];
38
- const okPrefix = prefixes.some((p) => c.startsWith(p));
39
- const okPattern = patterns.some((p) => new RegExp(p).test(c));
40
- if (!okPrefix && !okPattern) {
41
- throw new Error(`拒绝: 命令不在白名单 [${c}]`);
64
+ for (const seg of splitPipeline(c)) {
65
+ const ok = prefixes.some((p) => seg.startsWith(p))
66
+ || patterns.some((p) => new RegExp(p).test(seg));
67
+ if (!ok) {
68
+ throw new Error(`拒绝: 命令段不在白名单 [${seg}](整条: ${c})`);
69
+ }
42
70
  }
43
71
  for (const pat of FORBIDDEN_PATTERNS) {
44
72
  if (pat.test(c)) throw new Error(`拒绝: 命中禁止模式 [${pat}] 命令 [${c}]`);
45
73
  }
74
+ // 重定向白名单化:剥离合法的 N>&M(如 2>&1)后,任何残留的 > 一律拒绝
75
+ // (覆盖 > file、>> file、>| 强制覆盖、>&file 等全部写文件形态)
76
+ const stripped = c.replace(/\d*>&\d+/g, "");
77
+ if (stripped.includes(">")) {
78
+ throw new Error(`拒绝: 命中禁止模式 [输出重定向] 命令 [${c}]`);
79
+ }
80
+ // 敏感路径核查:命令中出现的任何绝对路径都不得命中 blocked_paths
81
+ // (让 exec-only / diff / find 与 read-file/dir 受同一份路径黑名单约束)
82
+ const blocked = conf.blocked_paths || [];
83
+ for (const p of extractAbsPaths(c)) {
84
+ for (const sp of blocked) {
85
+ if (p.startsWith(sp) || `${p}/` === sp) {
86
+ throw new Error(`拒绝: 敏感路径 [${p}](整条: ${c})`);
87
+ }
88
+ }
89
+ }
46
90
  return c;
47
91
  }
48
92
 
49
- const PATH_RE = /^[A-Za-z0-9._\-/:*]+$/;
93
+ const PATH_RE = /^[A-Za-z0-9._\-/:]+$/; // 无通配符:glob 展开可绕过黑名单(/etc/*/shadow)
94
+ const ARG_RE = /^[A-Za-z0-9._\-/:*]+$/; // 参数允许 * (单引号内的 grep 正则量词,无注入面)
50
95
 
51
96
  function validatePath(pathArg, conf) {
52
97
  const raw = String(pathArg).trim();