@goodandready/dsh-approval-gate 0.1.17 → 0.1.18
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/CHANGELOG.md +8 -0
- package/lib/contract-guard.js +147 -0
- package/lib/dispatcher.js +209 -207
- package/lib/engine.js +48 -0
- package/lib/index.js +21 -1
- package/lib/inspect.js +573 -520
- package/lib/messages.js +39 -0
- package/lib/overengineering.js +97 -0
- package/lib/paths.js +378 -320
- package/package.json +1 -1
package/lib/inspect.js
CHANGED
|
@@ -1,521 +1,574 @@
|
|
|
1
|
-
// lib/inspect.js
|
|
2
|
-
// Pure command and path inspector for dsh-approval-gate.
|
|
3
|
-
// Resolves: #133, #125, #126, #103, #90, #65, #40
|
|
4
|
-
// No Cordis imports: unit tests run this without the harness.
|
|
5
|
-
|
|
6
|
-
import { checkEnvExfiltration } from "./inspect-env.js";
|
|
7
|
-
export { checkEnvExfiltration } from "./inspect-env.js";
|
|
8
|
-
import { DYNAMIC_MARKER, SPLIT_OPS, tokenize, readCommandSubstitution, readBacktick, readParameterExpansion } from "./tokenizer.js";
|
|
9
|
-
import { expandHome, isProtectedConfigPath, resolveSafePath, isPathTraversal, canonicalizeFsPath, normalizeSeparators } from "./paths.js";
|
|
10
|
-
import { askMessage, denyMessage, sessionTag } from "./messages.js";
|
|
11
|
-
import { inspectGitCommand } from "./parsers/git.js";
|
|
12
|
-
import { extractRedirects, inspectRedirectSecurity, inspectSilentErrors, inspectPipelines, isDevicePath } from "./parsers/shell-cmds.js";
|
|
13
|
-
import { isLoopbackCommand } from "./loopback.js";
|
|
14
|
-
import { isReviewerAgent, extractFilePath, DEFAULT_BASH_TOOL, DEFAULT_FILE_WRITE_TOOLS } from "./dispatcher.js";
|
|
15
|
-
|
|
16
|
-
export { DYNAMIC_MARKER, tokenize, splitSubcommands } from "./tokenizer.js";
|
|
17
|
-
export { askMessage, denyMessage, sessionTag } from "./messages.js";
|
|
18
|
-
export { expandHome, isProtectedConfigPath, resolveSafePath, isPathTraversal, canonicalizeFsPath, normalizeSeparators } from "./paths.js";
|
|
19
|
-
export { DEFAULT_BASH_TOOL, DEFAULT_FILE_WRITE_TOOLS } from "./dispatcher.js";
|
|
20
|
-
|
|
21
|
-
const PREFIXES = new Set(["then", "do", "{", "}", "!"]);
|
|
22
|
-
const SHELLS = new Set(["bash", "sh", "dash", "zsh", "ksh", "ash"]);
|
|
23
|
-
const INTERPRETERS = new Set(["python", "python2", "python3", "node", "nodejs", "perl", "ruby", "php"]);
|
|
24
|
-
const DB_CLIENTS = new Set([
|
|
25
|
-
"sqlite3", "mysql", "psql", "pg_restore", "mongo", "mongosh", "redis-cli", "clickhouse-client",
|
|
26
|
-
]);
|
|
27
|
-
const DESTRUCTIVE_SQL = /\b(?:ALTER|DROP|TRUNCATE|DELETE\s+FROM|CREATE\s+(?:TABLE|DATABASE|INDEX))\b/i;
|
|
28
|
-
const KILL_CMDS = new Set(["kill", "pkill", "killall"]);
|
|
29
|
-
const SYSTEMCTL_DENY = new Set([
|
|
30
|
-
"stop", "restart", "disable", "enable", "mask", "unmask",
|
|
31
|
-
"reboot", "halt", "poweroff", "shutdown", "kill",
|
|
32
|
-
]);
|
|
33
|
-
const SERVICE_DENY = new Set(["stop", "restart", "force-stop", "force-reload"]);
|
|
34
|
-
|
|
35
|
-
const DESTRUCTIVE_CMDS = new Set([
|
|
36
|
-
"rm", "chmod", "chown", "kill", "pkill", "systemctl", "service",
|
|
37
|
-
"git", "dd", "mkfs", "tee", "install", "mv", "cp", "sed",
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
export function basename(path) {
|
|
41
|
-
const parts = String(path).replace(/\\/g, "/").split("/");
|
|
42
|
-
return parts[parts.length - 1] || "";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function isProtectedPath(path, env = process.env) {
|
|
46
|
-
if (typeof path !== "string" || path.trim() === "") return false;
|
|
47
|
-
return isProtectedConfigPath(path, env);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function wordsOf(seg) {
|
|
51
|
-
return seg.filter((t) => t.kind === "word").map((t) => t.value);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function wordTokensOf(seg) {
|
|
55
|
-
return seg.filter((t) => t.kind === "word");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function splitSegments(tokens) {
|
|
59
|
-
const segs = [];
|
|
60
|
-
let cur = [];
|
|
61
|
-
for (const t of tokens) {
|
|
62
|
-
if (t.kind === "op" && SPLIT_OPS.has(t.value)) {
|
|
63
|
-
segs.push(cur);
|
|
64
|
-
cur = [];
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
cur.push(t);
|
|
68
|
-
}
|
|
69
|
-
segs.push(cur);
|
|
70
|
-
return segs;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function stripPrefixes(argv) {
|
|
74
|
-
const out = argv.filter((a) => a !== "}" && a !== ";");
|
|
75
|
-
while (out.length) {
|
|
76
|
-
const head = out[0];
|
|
77
|
-
if (PREFIXES.has(head)) { out.shift(); continue; }
|
|
78
|
-
if (head === "env") {
|
|
79
|
-
out.shift();
|
|
80
|
-
while (out.length && out[0].includes("=") && out[0][0] !== "-") out.shift();
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (head.includes("=") && head[0] !== "-") { out.shift(); continue; }
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function hasLetterFlag(argv, letter) {
|
|
90
|
-
for (const a of argv) {
|
|
91
|
-
if (a === "--") break;
|
|
92
|
-
if (a.length > 1 && a[0] === "-" && a[1] !== "-" && a.indexOf(letter) !== -1) return true;
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function deny(reason, snippet) {
|
|
98
|
-
return { deny: true, ask: false, reason, detail: reason, snippet: String(snippet || reason).slice(0, 160) };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function allow() {
|
|
102
|
-
return { deny: false, ask: false };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function ask(reason, detail, snippet) {
|
|
106
|
-
return { deny: false, ask: true, reason, detail: detail || reason, snippet: String(snippet || "").slice(0, 160) };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function scriptAfterDashC(rest) {
|
|
110
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
111
|
-
const a = rest[i];
|
|
112
|
-
if (a === "-c") return rest[i + 1];
|
|
113
|
-
if (a[0] === "-" && a[1] !== "-" && a.indexOf("c") !== -1) return rest[i + 1];
|
|
114
|
-
}
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function skipWrapper(rest, arity) {
|
|
119
|
-
let i = 0;
|
|
120
|
-
while (i < rest.length && rest[i][0] === "-") {
|
|
121
|
-
const a = rest[i];
|
|
122
|
-
if (a === "--") { i += 1; break; }
|
|
123
|
-
if (arity.has(a)) i += 2;
|
|
124
|
-
else i += 1;
|
|
125
|
-
}
|
|
126
|
-
return rest.slice(i);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function inspectInterpreterPayload(cmd, payload) {
|
|
130
|
-
const code = String(payload || "");
|
|
131
|
-
const executesProcess = /(?:\bos\.system\s*\(|\bsubprocess\.(?:run|Popen|call|check_call|check_output)\s*\(|\bchild_process\.(?:exec|execSync|spawn|spawnSync)\s*\(|\b(?:exec|spawn)\s*\()/i.test(code);
|
|
132
|
-
const writesFile = /\b(?:write_text|write_bytes|unlink|rmdir|remove|chmod|chown|truncate)\s*\(|\bopen\s*\([^)]*,\s*["'][wax+]/i.test(code);
|
|
133
|
-
if (executesProcess) {
|
|
134
|
-
if (/\brm\s+(?:-[a-z]*r[a-z]*|--recursive)\s+|\b(?:kill|pkill|killall)\b|\bsystemctl\s+(?:stop|restart|disable|mask)\b|\bservice\s+\S+\s+(?:stop|restart)\b|\bgit\s+reset\s+--hard\b|\b(?:curl|wget)\b[^|]*\|\s*(?:bash|sh)\b|\bmkfs(?:\.|\s)|\bdd\b[^\n]{0,160}\bof=\/dev\/(?:sd|hd|vd|xvd|nvme|mmcblk|loop|mapper)/i.test(code)) {
|
|
135
|
-
return deny("interpreter-payload", code);
|
|
136
|
-
}
|
|
137
|
-
return ask("interpreter-payload", "interpreter process execution is not fully analyzed", code);
|
|
138
|
-
}
|
|
139
|
-
if (writesFile) {
|
|
140
|
-
if (/(?:\.env(?:\.[A-Za-z0-9_.-]+)?|credentials\.ya?ml|settings\.ya?ml|cordis\.patch\.yml|id_(?:rsa|ed25519|ecdsa)|[A-Za-z0-9_.-]+\.key)\b/i.test(code)) return deny("secret-write", code);
|
|
141
|
-
return ask("interpreter-payload", "interpreter file writes are not fully analyzed", code);
|
|
142
|
-
}
|
|
143
|
-
return allow();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function inspectArgv(argv, depth, hasHeredoc, config = {}) {
|
|
147
|
-
if (!argv.length) return allow();
|
|
148
|
-
const cmd = basename(argv[0]);
|
|
149
|
-
const rest = argv.slice(1);
|
|
150
|
-
|
|
151
|
-
const envHit = checkEnvExfiltration(argv, argv.join(" "));
|
|
152
|
-
if (envHit.deny) {
|
|
153
|
-
return deny("env-dump-leak", envHit.detail || argv.join(" "));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (cmd === "env") {
|
|
157
|
-
const sub = rest.filter((a) => !a.startsWith("-") && !a.includes("="));
|
|
158
|
-
if (sub.length > 0) {
|
|
159
|
-
return inspectArgv(sub, depth, hasHeredoc, config);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (cmd === "sudo" || cmd === "command" || cmd === "builtin" || cmd === "nohup" || cmd === "time" || cmd === "nice" || cmd === "ionice" || cmd === "stdbuf" || cmd === "timeout") {
|
|
164
|
-
const arity = new Set(["-u", "--user", "-g", "--group", "-p", "-s", "--signal", "-k", "--kill-after", "-o", "-e", "-i"]);
|
|
165
|
-
let next = skipWrapper(rest, arity);
|
|
166
|
-
if (cmd === "timeout" && next.length) next = next.slice(1);
|
|
167
|
-
if (cmd === "nice" && next.length && /^[0-9-]+$/.test(next[0])) next = next.slice(1);
|
|
168
|
-
return inspectArgv(next, depth, hasHeredoc, config);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (cmd === "eval") return deny("eval", argv.join(" "));
|
|
172
|
-
if (KILL_CMDS.has(cmd)) return deny("process-signal", argv.join(" "));
|
|
173
|
-
if (cmd === "rm" && (hasLetterFlag(argv, "r") || hasLetterFlag(argv, "R") || rest.includes("--recursive"))) {
|
|
174
|
-
return deny("recursive-rm", argv.join(" "));
|
|
175
|
-
}
|
|
176
|
-
if (cmd === "systemctl") {
|
|
177
|
-
const verb = rest.find((a) => a[0] !== "-");
|
|
178
|
-
if (verb && SYSTEMCTL_DENY.has(verb)) return deny("service-control", argv.join(" "));
|
|
179
|
-
}
|
|
180
|
-
if (cmd === "service") {
|
|
181
|
-
const verb = rest.length >= 2 ? rest[1] : "";
|
|
182
|
-
if (SERVICE_DENY.has(verb)) return deny("service-control", argv.join(" "));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Network commands & loopback bypass (#40)
|
|
186
|
-
if (cmd === "curl" || cmd === "wget") {
|
|
187
|
-
const loopback = isLoopbackCommand(argv);
|
|
188
|
-
if (!loopback.isLoopback && rest.some((a) => a.includes(DYNAMIC_MARKER) && /\b(?:authorization|proxy-authorization)\s*:\s*(?:bearer|token)\s+/i.test(a))) {
|
|
189
|
-
return ask("credential-in-argv", "expanded authorization values become curl process arguments", argv.join(" "));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Git inspection via specialized parser (#125, #103)
|
|
194
|
-
if (cmd === "git") {
|
|
195
|
-
const git = inspectGitCommand(argv);
|
|
196
|
-
if (git.isDestructive) {
|
|
197
|
-
return deny(git.reason, argv.join(" "));
|
|
198
|
-
}
|
|
199
|
-
if (git.verb === "push") {
|
|
200
|
-
return ask("git-remote-push", git.detail || "Publishing commits to a remote repository via git push requires confirmation", argv.join(" "));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (cmd === "find" && rest.includes("-delete")) return deny("find-delete", argv.join(" "));
|
|
205
|
-
if (DB_CLIENTS.has(cmd) && DESTRUCTIVE_SQL.test(rest.join(" "))) {
|
|
206
|
-
return deny("destructive-sql", argv.join(" "));
|
|
207
|
-
}
|
|
208
|
-
if (/^mkfs(?:\.|$)/i.test(cmd)) return deny("mkfs-device", argv.join(" "));
|
|
209
|
-
if (cmd === "dd") {
|
|
210
|
-
for (const a of rest) {
|
|
211
|
-
if (a.startsWith("of=") && (isDevicePath(a.slice(3)) || isProtectedPath(a.slice(3)))) return deny("device-write", a);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// File mutations & chmod/chown protection (#65)
|
|
216
|
-
if (cmd === "tee" || cmd === "install" || cmd === "cp" || cmd === "mv" || cmd === "truncate" || cmd === "chmod" || cmd === "chown") {
|
|
217
|
-
for (const a of rest) {
|
|
218
|
-
if (a[0] !== "-" && isProtectedPath(a)) return deny("secret-write", argv.join(" "));
|
|
219
|
-
}
|
|
220
|
-
if (cmd === "chmod" || cmd === "chown") {
|
|
221
|
-
const isRecursive = hasLetterFlag(argv, "R") || hasLetterFlag(argv, "r") || rest.includes("--recursive");
|
|
222
|
-
if (isRecursive) {
|
|
223
|
-
for (const a of rest) {
|
|
224
|
-
if (a === "/" || a === "/etc" || a === "/var" || a === "/usr" || a === "/root" || a === "/boot") {
|
|
225
|
-
return deny("device-write", argv.join(" "));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return deny("chmod-chown", argv.join(" "));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (cmd === "sed" && (hasLetterFlag(argv, "i") || rest.some((a) => a === "--in-place" || a.startsWith("--in-place=")))) {
|
|
234
|
-
for (const a of rest) {
|
|
235
|
-
if (a[0] !== "-" && isProtectedPath(a)) return deny("secret-write", argv.join(" "));
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (SHELLS.has(cmd) || cmd === "source" || cmd === ".") {
|
|
240
|
-
const script = scriptAfterDashC(rest);
|
|
241
|
-
if (typeof script === "string") {
|
|
242
|
-
if (depth > 4) return deny("nested-shell", script);
|
|
243
|
-
return inspectBashCommand(script, depth + 1, config);
|
|
244
|
-
}
|
|
245
|
-
const scriptFile = rest.find((a) => a && a[0] !== "-");
|
|
246
|
-
if (scriptFile) return ask("script-file", "shell script contents are not available to the guard", scriptFile);
|
|
247
|
-
if (!hasHeredoc) return ask("shell-stdin", "shell input is not fully available to the guard", argv.join(" "));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (INTERPRETERS.has(cmd) && rest.some((a) => a === "-c" || a === "-e")) {
|
|
251
|
-
const idx = rest.findIndex((a) => a === "-c" || a === "-e");
|
|
252
|
-
return inspectInterpreterPayload(cmd, rest[idx + 1] || "");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (/\.sh$/i.test(cmd) || (cmd.includes("/") && /\.(?:bash|zsh|ksh)$/i.test(cmd))) {
|
|
256
|
-
return ask("script-file", "script contents are not available to the guard", argv[0]);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return allow();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function executableTokenOf(seg) {
|
|
263
|
-
const words = wordTokensOf(seg);
|
|
264
|
-
const wrappers = new Set(["sudo", "command", "builtin", "nohup", "time", "nice", "ionice", "stdbuf", "timeout", "exec"]);
|
|
265
|
-
const takesValue = new Set(["-u", "--user", "-g", "--group", "-p", "-s", "--signal", "-k", "--kill-after", "-o", "-e", "-i"]);
|
|
266
|
-
let i = 0;
|
|
267
|
-
while (i < words.length) {
|
|
268
|
-
const value = words[i].value;
|
|
269
|
-
if (PREFIXES.has(value)) { i += 1; continue; }
|
|
270
|
-
if (value === "env") {
|
|
271
|
-
i += 1;
|
|
272
|
-
while (i < words.length && words[i].value.includes("=") && !words[i].value.startsWith("-")) i += 1;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (value.includes("=") && !value.startsWith("-")) { i += 1; continue; }
|
|
276
|
-
if (!wrappers.has(value)) return words[i];
|
|
277
|
-
i += 1;
|
|
278
|
-
while (i < words.length && words[i].value.startsWith("-")) {
|
|
279
|
-
const flag = words[i].value;
|
|
280
|
-
if (flag === "--") { i += 1; break; }
|
|
281
|
-
if (takesValue.has(flag)) i += 2;
|
|
282
|
-
else i += 1;
|
|
283
|
-
}
|
|
284
|
-
if (value === "timeout" && i < words.length) i += 1;
|
|
285
|
-
if (value === "nice" && i < words.length && /^[0-9-]+$/.test(words[i].value)) i += 1;
|
|
286
|
-
}
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function commandNameFromSegment(seg) {
|
|
291
|
-
const token = executableTokenOf(seg);
|
|
292
|
-
return token ? basename(token.value) : "";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function inspectEmbeddedExpansions(source, depth, config = {}) {
|
|
296
|
-
if (depth > 8) return ask("unparseable", "nesting-depth", source);
|
|
297
|
-
for (let i = 0; i < source.length; i += 1) {
|
|
298
|
-
if (source[i] === "\\") { i += 1; continue; }
|
|
299
|
-
if (source.startsWith("$" + "(", i) || source[i] === String.fromCharCode(96)) {
|
|
300
|
-
const sub = source.startsWith("$" + "(", i)
|
|
301
|
-
? readCommandSubstitution(source, i)
|
|
302
|
-
: readBacktick(source, i);
|
|
303
|
-
if (!sub) return ask("unparseable", "unclosed-command-substitution", source.slice(i));
|
|
304
|
-
const nested = inspectBashCommand(sub.script, depth + 1, config);
|
|
305
|
-
if (nested.deny || nested.ask) return nested;
|
|
306
|
-
i = sub.end - 1;
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
if (source.startsWith("$" + "{", i)) {
|
|
310
|
-
const expansion = readParameterExpansion(source, i);
|
|
311
|
-
if (!expansion) return ask("unparseable", "unclosed-parameter-expansion", source.slice(i));
|
|
312
|
-
if (expansion.body !== undefined) {
|
|
313
|
-
const nested = inspectEmbeddedExpansions(expansion.body, depth + 1, config);
|
|
314
|
-
if (nested.deny || nested.ask) return nested;
|
|
315
|
-
}
|
|
316
|
-
i = expansion.end - 1;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return allow();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function inspectPipelineHeredocs(tokens, depth, config = {}) {
|
|
323
|
-
let group = [];
|
|
324
|
-
let segment = [];
|
|
325
|
-
function finishGroup() {
|
|
326
|
-
if (segment.length) group.push(segment);
|
|
327
|
-
const hasShell = group.some((part) => SHELLS.has(commandNameFromSegment(part)));
|
|
328
|
-
if (hasShell) {
|
|
329
|
-
for (const part of group) {
|
|
330
|
-
for (const heredoc of part.filter((t) => t.kind === "heredoc")) {
|
|
331
|
-
const nested = inspectBashCommand(heredoc.body, depth + 1, config);
|
|
332
|
-
if (nested.deny || nested.ask) return nested;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
group = [];
|
|
337
|
-
segment = [];
|
|
338
|
-
return allow();
|
|
339
|
-
}
|
|
340
|
-
for (const token of tokens) {
|
|
341
|
-
if (token.kind === "op" && SPLIT_OPS.has(token.value)) {
|
|
342
|
-
group.push(segment);
|
|
343
|
-
segment = [];
|
|
344
|
-
if (token.value !== "|" && token.value !== "|&") {
|
|
345
|
-
const result = finishGroup();
|
|
346
|
-
if (result.deny || result.ask) return result;
|
|
347
|
-
}
|
|
348
|
-
} else {
|
|
349
|
-
segment.push(token);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
group.push(segment);
|
|
353
|
-
return finishGroup();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function inspectUnparsedDanger(command) {
|
|
357
|
-
const checks = [
|
|
358
|
-
["curl-pipe", /\b(?:curl|wget)\b[^\n]{0,240}\|\s*(?:sudo\s+)?(?:bash|sh)\b/i],
|
|
359
|
-
["recursive-rm", /\brm\s+(?:-[^\s]*r[^\s]*|--recursive)(?:\s|$)/i],
|
|
360
|
-
["process-signal", /\b(?:kill|pkill|killall)\b/i],
|
|
361
|
-
["service-control", /\bsystemctl\s+(?:stop|restart|disable|enable|mask|unmask|reboot|halt|poweroff|shutdown|kill)\b|\bservice\s+\S+\s+(?:stop|restart|force-stop|force-reload)\b/i],
|
|
362
|
-
["git-reset-hard", /\bgit\b[^\n;|&]{0,120}\breset\b[^\n;|&]{0,80}--hard\b/i],
|
|
363
|
-
["git-clean", /\bgit\b[^\n;|&]{0,120}\bclean\b[^\n;|&]{0,80}(?:--force|(?:^|\s)-[^\s]*f[^\s]*)/i],
|
|
364
|
-
["find-delete", /\bfind\b[^\n;|&]{0,200}\s-delete\b/i],
|
|
365
|
-
["mkfs-device", /\bmkfs(?:\.[A-Za-z0-9_-]+)?\b/i],
|
|
366
|
-
["device-write", /\bdd\b[^\n;|&]{0,200}\bof=\/dev\/(?:sd[a-z]+|hd[a-z]+|vd[a-z]+|xvd[a-z]+|nvme\d+n\d+(?:p\d+)?|mmcblk\d+(?:p\d+)?|loop\d+|mapper\/\S+)/i],
|
|
367
|
-
["destructive-sql", /\b(?:sqlite3|mysql|psql|mongo|mongosh|clickhouse-client)\b[^\n;|&]{0,200}\b(?:ALTER|DROP|TRUNCATE|DELETE\s+FROM|CREATE\s+(?:TABLE|DATABASE|INDEX))\b/i],
|
|
368
|
-
["secret-write", /(?:>|&>|\b(?:tee|install|mv|truncate|chmod|chown)\b|\bsed\b[^\n;|&]{0,160}(?:-i|--in-place))[^\n;|&]{0,240}(?:\.env(?:\.[\w.-]+)?|credentials\.ya?ml|settings\.ya?ml|cordis\.patch\.yml|[\w.-]*(?:secret|token|credential|api[_-]?key)[\w.-]*|id_(?:rsa|ed25519|ecdsa)|[\w.-]+\.key)\b/i],
|
|
369
|
-
];
|
|
370
|
-
for (const [reason, pattern] of checks) {
|
|
371
|
-
if (pattern.test(command)) return deny(reason, command);
|
|
372
|
-
}
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export function inspectBashCommand(command, depth = 0, config = {}) {
|
|
377
|
-
if (depth === undefined) depth = 0;
|
|
378
|
-
if (depth > 8) return ask("unparseable", "nesting-depth", String(command || ""));
|
|
379
|
-
if (typeof command !== "string") return ask("unknown-format", "command is not a string", "non-string command");
|
|
380
|
-
if (command.trim() === "") return allow();
|
|
381
|
-
|
|
382
|
-
const tok = tokenize(command);
|
|
383
|
-
const tokens = tok.tokens || [];
|
|
384
|
-
|
|
385
|
-
if (!tok.ok) {
|
|
386
|
-
const knownDanger = inspectUnparsedDanger(command);
|
|
387
|
-
if (knownDanger) return knownDanger;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Pipeline inspection (#126)
|
|
391
|
-
const pipeCheck = inspectPipelines(tokens);
|
|
392
|
-
if (pipeCheck.dangerousPipeline) {
|
|
393
|
-
return deny(pipeCheck.reason, command);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const pipelineHeredocHit = inspectPipelineHeredocs(tokens, depth, config);
|
|
397
|
-
if (pipelineHeredocHit.deny || pipelineHeredocHit.ask) return pipelineHeredocHit;
|
|
398
|
-
|
|
399
|
-
// Silent error guard check (#90)
|
|
400
|
-
const silentHit = inspectSilentErrors(command);
|
|
401
|
-
|
|
402
|
-
const segs = splitSegments(tokens);
|
|
403
|
-
for (const seg of segs) {
|
|
404
|
-
const rawWords = wordsOf(seg);
|
|
405
|
-
const envHit = checkEnvExfiltration(rawWords, command);
|
|
406
|
-
if (envHit.deny) {
|
|
407
|
-
return deny("env-dump-leak", envHit.detail || command);
|
|
408
|
-
}
|
|
409
|
-
const wordTokens = wordTokensOf(seg);
|
|
410
|
-
const heredocs = seg.filter((t) => t.kind === "heredoc");
|
|
411
|
-
const argv = stripPrefixes(wordsOf(seg));
|
|
412
|
-
const commandToken = executableTokenOf(seg);
|
|
413
|
-
const commandName = commandNameFromSegment(seg);
|
|
414
|
-
|
|
415
|
-
// Silent error guard triggered on mutating system commands (#90)
|
|
416
|
-
if (silentHit.masked && (config.silentErrorGuard || DESTRUCTIVE_CMDS.has(commandName))) {
|
|
417
|
-
return ask("silent-error-masked", silentHit.detail, command);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
for (const token of wordTokens) {
|
|
421
|
-
for (const substitution of token.substitutions || []) {
|
|
422
|
-
const nested = substitution.kind === "parameter-expansion"
|
|
423
|
-
? inspectEmbeddedExpansions(substitution.script, depth + 1, config)
|
|
424
|
-
: inspectBashCommand(substitution.script, depth + 1, config);
|
|
425
|
-
if (nested.deny || nested.ask) return nested;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Redirects security inspection (#126)
|
|
430
|
-
const redirects = extractRedirects(seg);
|
|
431
|
-
const redirectHit = inspectRedirectSecurity(redirects, {
|
|
432
|
-
workspaceDir: config.workspaceDir,
|
|
433
|
-
env: config.env,
|
|
434
|
-
});
|
|
435
|
-
if (redirectHit) {
|
|
436
|
-
return redirectHit.deny ? deny(redirectHit.reason, redirectHit.path) : ask(redirectHit.reason, redirectHit.detail, redirectHit.path);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (commandToken && commandToken.dynamic && !commandToken.value.includes("=")) {
|
|
440
|
-
return ask("dynamic-command", "expanded command name cannot be verified", commandToken.value);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const dynamicArgs = wordTokens.some((t) => (
|
|
444
|
-
t.dynamic && (!commandToken || t !== commandToken) && !t.value.includes("=")
|
|
445
|
-
));
|
|
446
|
-
if (dynamicArgs && ["rm", "find", "git", "systemctl", "service", "tee", "install", "cp", "mv", "truncate", "sed", "dd"].includes(commandName)) {
|
|
447
|
-
return ask("dynamic-arguments", "expanded command arguments cannot be verified", commandName);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (heredocs.length) {
|
|
451
|
-
for (const heredoc of heredocs) {
|
|
452
|
-
if (!heredoc.quoted) {
|
|
453
|
-
const expansionHit = inspectEmbeddedExpansions(heredoc.body, depth, config);
|
|
454
|
-
if (expansionHit.deny || expansionHit.ask) return expansionHit;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (SHELLS.has(commandName)) {
|
|
458
|
-
for (const heredoc of heredocs) {
|
|
459
|
-
const nested = inspectBashCommand(heredoc.body, depth + 1, config);
|
|
460
|
-
if (nested.deny || nested.ask) return nested;
|
|
461
|
-
}
|
|
462
|
-
} else if (INTERPRETERS.has(commandName)) {
|
|
463
|
-
for (const heredoc of heredocs) {
|
|
464
|
-
const nested = inspectInterpreterPayload(commandName, heredoc.body);
|
|
465
|
-
if (nested.deny || nested.ask) return nested;
|
|
466
|
-
}
|
|
467
|
-
} else if (DB_CLIENTS.has(commandName)) {
|
|
468
|
-
for (const heredoc of heredocs) {
|
|
469
|
-
if (DESTRUCTIVE_SQL.test(heredoc.body)) {
|
|
470
|
-
return deny("destructive-sql", heredoc.body);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const hit = inspectArgv(argv, depth, heredocs.length > 0, config);
|
|
477
|
-
if (hit.deny || hit.ask) return hit;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (!tok.ok) return ask("unparseable", tok.reason || "unsupported shell syntax", tok.snippet || command);
|
|
481
|
-
return allow();
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
1
|
+
// lib/inspect.js
|
|
2
|
+
// Pure command and path inspector for dsh-approval-gate.
|
|
3
|
+
// Resolves: #133, #125, #126, #103, #90, #65, #40
|
|
4
|
+
// No Cordis imports: unit tests run this without the harness.
|
|
5
|
+
|
|
6
|
+
import { checkEnvExfiltration } from "./inspect-env.js";
|
|
7
|
+
export { checkEnvExfiltration } from "./inspect-env.js";
|
|
8
|
+
import { DYNAMIC_MARKER, SPLIT_OPS, tokenize, readCommandSubstitution, readBacktick, readParameterExpansion } from "./tokenizer.js";
|
|
9
|
+
import { expandHome, isProtectedConfigPath, resolveSafePath, isPathTraversal, canonicalizeFsPath, normalizeSeparators, checkTempIsolation } from "./paths.js";
|
|
10
|
+
import { askMessage, denyMessage, sessionTag } from "./messages.js";
|
|
11
|
+
import { inspectGitCommand } from "./parsers/git.js";
|
|
12
|
+
import { extractRedirects, inspectRedirectSecurity, inspectSilentErrors, inspectPipelines, isDevicePath } from "./parsers/shell-cmds.js";
|
|
13
|
+
import { isLoopbackCommand } from "./loopback.js";
|
|
14
|
+
import { isReviewerAgent, extractFilePath, DEFAULT_BASH_TOOL, DEFAULT_FILE_WRITE_TOOLS } from "./dispatcher.js";
|
|
15
|
+
|
|
16
|
+
export { DYNAMIC_MARKER, tokenize, splitSubcommands } from "./tokenizer.js";
|
|
17
|
+
export { askMessage, denyMessage, sessionTag } from "./messages.js";
|
|
18
|
+
export { expandHome, isProtectedConfigPath, resolveSafePath, isPathTraversal, canonicalizeFsPath, normalizeSeparators } from "./paths.js";
|
|
19
|
+
export { DEFAULT_BASH_TOOL, DEFAULT_FILE_WRITE_TOOLS } from "./dispatcher.js";
|
|
20
|
+
|
|
21
|
+
const PREFIXES = new Set(["then", "do", "{", "}", "!"]);
|
|
22
|
+
const SHELLS = new Set(["bash", "sh", "dash", "zsh", "ksh", "ash"]);
|
|
23
|
+
const INTERPRETERS = new Set(["python", "python2", "python3", "node", "nodejs", "perl", "ruby", "php"]);
|
|
24
|
+
const DB_CLIENTS = new Set([
|
|
25
|
+
"sqlite3", "mysql", "psql", "pg_restore", "mongo", "mongosh", "redis-cli", "clickhouse-client",
|
|
26
|
+
]);
|
|
27
|
+
const DESTRUCTIVE_SQL = /\b(?:ALTER|DROP|TRUNCATE|DELETE\s+FROM|CREATE\s+(?:TABLE|DATABASE|INDEX))\b/i;
|
|
28
|
+
const KILL_CMDS = new Set(["kill", "pkill", "killall"]);
|
|
29
|
+
const SYSTEMCTL_DENY = new Set([
|
|
30
|
+
"stop", "restart", "disable", "enable", "mask", "unmask",
|
|
31
|
+
"reboot", "halt", "poweroff", "shutdown", "kill",
|
|
32
|
+
]);
|
|
33
|
+
const SERVICE_DENY = new Set(["stop", "restart", "force-stop", "force-reload"]);
|
|
34
|
+
|
|
35
|
+
const DESTRUCTIVE_CMDS = new Set([
|
|
36
|
+
"rm", "chmod", "chown", "kill", "pkill", "systemctl", "service",
|
|
37
|
+
"git", "dd", "mkfs", "tee", "install", "mv", "cp", "sed",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export function basename(path) {
|
|
41
|
+
const parts = String(path).replace(/\\/g, "/").split("/");
|
|
42
|
+
return parts[parts.length - 1] || "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isProtectedPath(path, env = process.env) {
|
|
46
|
+
if (typeof path !== "string" || path.trim() === "") return false;
|
|
47
|
+
return isProtectedConfigPath(path, env);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function wordsOf(seg) {
|
|
51
|
+
return seg.filter((t) => t.kind === "word").map((t) => t.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wordTokensOf(seg) {
|
|
55
|
+
return seg.filter((t) => t.kind === "word");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function splitSegments(tokens) {
|
|
59
|
+
const segs = [];
|
|
60
|
+
let cur = [];
|
|
61
|
+
for (const t of tokens) {
|
|
62
|
+
if (t.kind === "op" && SPLIT_OPS.has(t.value)) {
|
|
63
|
+
segs.push(cur);
|
|
64
|
+
cur = [];
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
cur.push(t);
|
|
68
|
+
}
|
|
69
|
+
segs.push(cur);
|
|
70
|
+
return segs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stripPrefixes(argv) {
|
|
74
|
+
const out = argv.filter((a) => a !== "}" && a !== ";");
|
|
75
|
+
while (out.length) {
|
|
76
|
+
const head = out[0];
|
|
77
|
+
if (PREFIXES.has(head)) { out.shift(); continue; }
|
|
78
|
+
if (head === "env") {
|
|
79
|
+
out.shift();
|
|
80
|
+
while (out.length && out[0].includes("=") && out[0][0] !== "-") out.shift();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (head.includes("=") && head[0] !== "-") { out.shift(); continue; }
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasLetterFlag(argv, letter) {
|
|
90
|
+
for (const a of argv) {
|
|
91
|
+
if (a === "--") break;
|
|
92
|
+
if (a.length > 1 && a[0] === "-" && a[1] !== "-" && a.indexOf(letter) !== -1) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function deny(reason, snippet) {
|
|
98
|
+
return { deny: true, ask: false, reason, detail: reason, snippet: String(snippet || reason).slice(0, 160) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function allow() {
|
|
102
|
+
return { deny: false, ask: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ask(reason, detail, snippet) {
|
|
106
|
+
return { deny: false, ask: true, reason, detail: detail || reason, snippet: String(snippet || "").slice(0, 160) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function scriptAfterDashC(rest) {
|
|
110
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
111
|
+
const a = rest[i];
|
|
112
|
+
if (a === "-c") return rest[i + 1];
|
|
113
|
+
if (a[0] === "-" && a[1] !== "-" && a.indexOf("c") !== -1) return rest[i + 1];
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function skipWrapper(rest, arity) {
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < rest.length && rest[i][0] === "-") {
|
|
121
|
+
const a = rest[i];
|
|
122
|
+
if (a === "--") { i += 1; break; }
|
|
123
|
+
if (arity.has(a)) i += 2;
|
|
124
|
+
else i += 1;
|
|
125
|
+
}
|
|
126
|
+
return rest.slice(i);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function inspectInterpreterPayload(cmd, payload) {
|
|
130
|
+
const code = String(payload || "");
|
|
131
|
+
const executesProcess = /(?:\bos\.system\s*\(|\bsubprocess\.(?:run|Popen|call|check_call|check_output)\s*\(|\bchild_process\.(?:exec|execSync|spawn|spawnSync)\s*\(|\b(?:exec|spawn)\s*\()/i.test(code);
|
|
132
|
+
const writesFile = /\b(?:write_text|write_bytes|unlink|rmdir|remove|chmod|chown|truncate)\s*\(|\bopen\s*\([^)]*,\s*["'][wax+]/i.test(code);
|
|
133
|
+
if (executesProcess) {
|
|
134
|
+
if (/\brm\s+(?:-[a-z]*r[a-z]*|--recursive)\s+|\b(?:kill|pkill|killall)\b|\bsystemctl\s+(?:stop|restart|disable|mask)\b|\bservice\s+\S+\s+(?:stop|restart)\b|\bgit\s+reset\s+--hard\b|\b(?:curl|wget)\b[^|]*\|\s*(?:bash|sh)\b|\bmkfs(?:\.|\s)|\bdd\b[^\n]{0,160}\bof=\/dev\/(?:sd|hd|vd|xvd|nvme|mmcblk|loop|mapper)/i.test(code)) {
|
|
135
|
+
return deny("interpreter-payload", code);
|
|
136
|
+
}
|
|
137
|
+
return ask("interpreter-payload", "interpreter process execution is not fully analyzed", code);
|
|
138
|
+
}
|
|
139
|
+
if (writesFile) {
|
|
140
|
+
if (/(?:\.env(?:\.[A-Za-z0-9_.-]+)?|credentials\.ya?ml|settings\.ya?ml|cordis\.patch\.yml|id_(?:rsa|ed25519|ecdsa)|[A-Za-z0-9_.-]+\.key)\b/i.test(code)) return deny("secret-write", code);
|
|
141
|
+
return ask("interpreter-payload", "interpreter file writes are not fully analyzed", code);
|
|
142
|
+
}
|
|
143
|
+
return allow();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function inspectArgv(argv, depth, hasHeredoc, config = {}) {
|
|
147
|
+
if (!argv.length) return allow();
|
|
148
|
+
const cmd = basename(argv[0]);
|
|
149
|
+
const rest = argv.slice(1);
|
|
150
|
+
|
|
151
|
+
const envHit = checkEnvExfiltration(argv, argv.join(" "));
|
|
152
|
+
if (envHit.deny) {
|
|
153
|
+
return deny("env-dump-leak", envHit.detail || argv.join(" "));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (cmd === "env") {
|
|
157
|
+
const sub = rest.filter((a) => !a.startsWith("-") && !a.includes("="));
|
|
158
|
+
if (sub.length > 0) {
|
|
159
|
+
return inspectArgv(sub, depth, hasHeredoc, config);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (cmd === "sudo" || cmd === "command" || cmd === "builtin" || cmd === "nohup" || cmd === "time" || cmd === "nice" || cmd === "ionice" || cmd === "stdbuf" || cmd === "timeout") {
|
|
164
|
+
const arity = new Set(["-u", "--user", "-g", "--group", "-p", "-s", "--signal", "-k", "--kill-after", "-o", "-e", "-i"]);
|
|
165
|
+
let next = skipWrapper(rest, arity);
|
|
166
|
+
if (cmd === "timeout" && next.length) next = next.slice(1);
|
|
167
|
+
if (cmd === "nice" && next.length && /^[0-9-]+$/.test(next[0])) next = next.slice(1);
|
|
168
|
+
return inspectArgv(next, depth, hasHeredoc, config);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (cmd === "eval") return deny("eval", argv.join(" "));
|
|
172
|
+
if (KILL_CMDS.has(cmd)) return deny("process-signal", argv.join(" "));
|
|
173
|
+
if (cmd === "rm" && (hasLetterFlag(argv, "r") || hasLetterFlag(argv, "R") || rest.includes("--recursive"))) {
|
|
174
|
+
return deny("recursive-rm", argv.join(" "));
|
|
175
|
+
}
|
|
176
|
+
if (cmd === "systemctl") {
|
|
177
|
+
const verb = rest.find((a) => a[0] !== "-");
|
|
178
|
+
if (verb && SYSTEMCTL_DENY.has(verb)) return deny("service-control", argv.join(" "));
|
|
179
|
+
}
|
|
180
|
+
if (cmd === "service") {
|
|
181
|
+
const verb = rest.length >= 2 ? rest[1] : "";
|
|
182
|
+
if (SERVICE_DENY.has(verb)) return deny("service-control", argv.join(" "));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Network commands & loopback bypass (#40)
|
|
186
|
+
if (cmd === "curl" || cmd === "wget") {
|
|
187
|
+
const loopback = isLoopbackCommand(argv);
|
|
188
|
+
if (!loopback.isLoopback && rest.some((a) => a.includes(DYNAMIC_MARKER) && /\b(?:authorization|proxy-authorization)\s*:\s*(?:bearer|token)\s+/i.test(a))) {
|
|
189
|
+
return ask("credential-in-argv", "expanded authorization values become curl process arguments", argv.join(" "));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Git inspection via specialized parser (#125, #103)
|
|
194
|
+
if (cmd === "git") {
|
|
195
|
+
const git = inspectGitCommand(argv);
|
|
196
|
+
if (git.isDestructive) {
|
|
197
|
+
return deny(git.reason, argv.join(" "));
|
|
198
|
+
}
|
|
199
|
+
if (git.verb === "push") {
|
|
200
|
+
return ask("git-remote-push", git.detail || "Publishing commits to a remote repository via git push requires confirmation", argv.join(" "));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (cmd === "find" && rest.includes("-delete")) return deny("find-delete", argv.join(" "));
|
|
205
|
+
if (DB_CLIENTS.has(cmd) && DESTRUCTIVE_SQL.test(rest.join(" "))) {
|
|
206
|
+
return deny("destructive-sql", argv.join(" "));
|
|
207
|
+
}
|
|
208
|
+
if (/^mkfs(?:\.|$)/i.test(cmd)) return deny("mkfs-device", argv.join(" "));
|
|
209
|
+
if (cmd === "dd") {
|
|
210
|
+
for (const a of rest) {
|
|
211
|
+
if (a.startsWith("of=") && (isDevicePath(a.slice(3)) || isProtectedPath(a.slice(3)))) return deny("device-write", a);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// File mutations & chmod/chown protection (#65)
|
|
216
|
+
if (cmd === "tee" || cmd === "install" || cmd === "cp" || cmd === "mv" || cmd === "truncate" || cmd === "chmod" || cmd === "chown") {
|
|
217
|
+
for (const a of rest) {
|
|
218
|
+
if (a[0] !== "-" && isProtectedPath(a)) return deny("secret-write", argv.join(" "));
|
|
219
|
+
}
|
|
220
|
+
if (cmd === "chmod" || cmd === "chown") {
|
|
221
|
+
const isRecursive = hasLetterFlag(argv, "R") || hasLetterFlag(argv, "r") || rest.includes("--recursive");
|
|
222
|
+
if (isRecursive) {
|
|
223
|
+
for (const a of rest) {
|
|
224
|
+
if (a === "/" || a === "/etc" || a === "/var" || a === "/usr" || a === "/root" || a === "/boot") {
|
|
225
|
+
return deny("device-write", argv.join(" "));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return deny("chmod-chown", argv.join(" "));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (cmd === "sed" && (hasLetterFlag(argv, "i") || rest.some((a) => a === "--in-place" || a.startsWith("--in-place=")))) {
|
|
234
|
+
for (const a of rest) {
|
|
235
|
+
if (a[0] !== "-" && isProtectedPath(a)) return deny("secret-write", argv.join(" "));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (SHELLS.has(cmd) || cmd === "source" || cmd === ".") {
|
|
240
|
+
const script = scriptAfterDashC(rest);
|
|
241
|
+
if (typeof script === "string") {
|
|
242
|
+
if (depth > 4) return deny("nested-shell", script);
|
|
243
|
+
return inspectBashCommand(script, depth + 1, config);
|
|
244
|
+
}
|
|
245
|
+
const scriptFile = rest.find((a) => a && a[0] !== "-");
|
|
246
|
+
if (scriptFile) return ask("script-file", "shell script contents are not available to the guard", scriptFile);
|
|
247
|
+
if (!hasHeredoc) return ask("shell-stdin", "shell input is not fully available to the guard", argv.join(" "));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (INTERPRETERS.has(cmd) && rest.some((a) => a === "-c" || a === "-e")) {
|
|
251
|
+
const idx = rest.findIndex((a) => a === "-c" || a === "-e");
|
|
252
|
+
return inspectInterpreterPayload(cmd, rest[idx + 1] || "");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (/\.sh$/i.test(cmd) || (cmd.includes("/") && /\.(?:bash|zsh|ksh)$/i.test(cmd))) {
|
|
256
|
+
return ask("script-file", "script contents are not available to the guard", argv[0]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return allow();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function executableTokenOf(seg) {
|
|
263
|
+
const words = wordTokensOf(seg);
|
|
264
|
+
const wrappers = new Set(["sudo", "command", "builtin", "nohup", "time", "nice", "ionice", "stdbuf", "timeout", "exec"]);
|
|
265
|
+
const takesValue = new Set(["-u", "--user", "-g", "--group", "-p", "-s", "--signal", "-k", "--kill-after", "-o", "-e", "-i"]);
|
|
266
|
+
let i = 0;
|
|
267
|
+
while (i < words.length) {
|
|
268
|
+
const value = words[i].value;
|
|
269
|
+
if (PREFIXES.has(value)) { i += 1; continue; }
|
|
270
|
+
if (value === "env") {
|
|
271
|
+
i += 1;
|
|
272
|
+
while (i < words.length && words[i].value.includes("=") && !words[i].value.startsWith("-")) i += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (value.includes("=") && !value.startsWith("-")) { i += 1; continue; }
|
|
276
|
+
if (!wrappers.has(value)) return words[i];
|
|
277
|
+
i += 1;
|
|
278
|
+
while (i < words.length && words[i].value.startsWith("-")) {
|
|
279
|
+
const flag = words[i].value;
|
|
280
|
+
if (flag === "--") { i += 1; break; }
|
|
281
|
+
if (takesValue.has(flag)) i += 2;
|
|
282
|
+
else i += 1;
|
|
283
|
+
}
|
|
284
|
+
if (value === "timeout" && i < words.length) i += 1;
|
|
285
|
+
if (value === "nice" && i < words.length && /^[0-9-]+$/.test(words[i].value)) i += 1;
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function commandNameFromSegment(seg) {
|
|
291
|
+
const token = executableTokenOf(seg);
|
|
292
|
+
return token ? basename(token.value) : "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function inspectEmbeddedExpansions(source, depth, config = {}) {
|
|
296
|
+
if (depth > 8) return ask("unparseable", "nesting-depth", source);
|
|
297
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
298
|
+
if (source[i] === "\\") { i += 1; continue; }
|
|
299
|
+
if (source.startsWith("$" + "(", i) || source[i] === String.fromCharCode(96)) {
|
|
300
|
+
const sub = source.startsWith("$" + "(", i)
|
|
301
|
+
? readCommandSubstitution(source, i)
|
|
302
|
+
: readBacktick(source, i);
|
|
303
|
+
if (!sub) return ask("unparseable", "unclosed-command-substitution", source.slice(i));
|
|
304
|
+
const nested = inspectBashCommand(sub.script, depth + 1, config);
|
|
305
|
+
if (nested.deny || nested.ask) return nested;
|
|
306
|
+
i = sub.end - 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (source.startsWith("$" + "{", i)) {
|
|
310
|
+
const expansion = readParameterExpansion(source, i);
|
|
311
|
+
if (!expansion) return ask("unparseable", "unclosed-parameter-expansion", source.slice(i));
|
|
312
|
+
if (expansion.body !== undefined) {
|
|
313
|
+
const nested = inspectEmbeddedExpansions(expansion.body, depth + 1, config);
|
|
314
|
+
if (nested.deny || nested.ask) return nested;
|
|
315
|
+
}
|
|
316
|
+
i = expansion.end - 1;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return allow();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function inspectPipelineHeredocs(tokens, depth, config = {}) {
|
|
323
|
+
let group = [];
|
|
324
|
+
let segment = [];
|
|
325
|
+
function finishGroup() {
|
|
326
|
+
if (segment.length) group.push(segment);
|
|
327
|
+
const hasShell = group.some((part) => SHELLS.has(commandNameFromSegment(part)));
|
|
328
|
+
if (hasShell) {
|
|
329
|
+
for (const part of group) {
|
|
330
|
+
for (const heredoc of part.filter((t) => t.kind === "heredoc")) {
|
|
331
|
+
const nested = inspectBashCommand(heredoc.body, depth + 1, config);
|
|
332
|
+
if (nested.deny || nested.ask) return nested;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
group = [];
|
|
337
|
+
segment = [];
|
|
338
|
+
return allow();
|
|
339
|
+
}
|
|
340
|
+
for (const token of tokens) {
|
|
341
|
+
if (token.kind === "op" && SPLIT_OPS.has(token.value)) {
|
|
342
|
+
group.push(segment);
|
|
343
|
+
segment = [];
|
|
344
|
+
if (token.value !== "|" && token.value !== "|&") {
|
|
345
|
+
const result = finishGroup();
|
|
346
|
+
if (result.deny || result.ask) return result;
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
segment.push(token);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
group.push(segment);
|
|
353
|
+
return finishGroup();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function inspectUnparsedDanger(command) {
|
|
357
|
+
const checks = [
|
|
358
|
+
["curl-pipe", /\b(?:curl|wget)\b[^\n]{0,240}\|\s*(?:sudo\s+)?(?:bash|sh)\b/i],
|
|
359
|
+
["recursive-rm", /\brm\s+(?:-[^\s]*r[^\s]*|--recursive)(?:\s|$)/i],
|
|
360
|
+
["process-signal", /\b(?:kill|pkill|killall)\b/i],
|
|
361
|
+
["service-control", /\bsystemctl\s+(?:stop|restart|disable|enable|mask|unmask|reboot|halt|poweroff|shutdown|kill)\b|\bservice\s+\S+\s+(?:stop|restart|force-stop|force-reload)\b/i],
|
|
362
|
+
["git-reset-hard", /\bgit\b[^\n;|&]{0,120}\breset\b[^\n;|&]{0,80}--hard\b/i],
|
|
363
|
+
["git-clean", /\bgit\b[^\n;|&]{0,120}\bclean\b[^\n;|&]{0,80}(?:--force|(?:^|\s)-[^\s]*f[^\s]*)/i],
|
|
364
|
+
["find-delete", /\bfind\b[^\n;|&]{0,200}\s-delete\b/i],
|
|
365
|
+
["mkfs-device", /\bmkfs(?:\.[A-Za-z0-9_-]+)?\b/i],
|
|
366
|
+
["device-write", /\bdd\b[^\n;|&]{0,200}\bof=\/dev\/(?:sd[a-z]+|hd[a-z]+|vd[a-z]+|xvd[a-z]+|nvme\d+n\d+(?:p\d+)?|mmcblk\d+(?:p\d+)?|loop\d+|mapper\/\S+)/i],
|
|
367
|
+
["destructive-sql", /\b(?:sqlite3|mysql|psql|mongo|mongosh|clickhouse-client)\b[^\n;|&]{0,200}\b(?:ALTER|DROP|TRUNCATE|DELETE\s+FROM|CREATE\s+(?:TABLE|DATABASE|INDEX))\b/i],
|
|
368
|
+
["secret-write", /(?:>|&>|\b(?:tee|install|mv|truncate|chmod|chown)\b|\bsed\b[^\n;|&]{0,160}(?:-i|--in-place))[^\n;|&]{0,240}(?:\.env(?:\.[\w.-]+)?|credentials\.ya?ml|settings\.ya?ml|cordis\.patch\.yml|[\w.-]*(?:secret|token|credential|api[_-]?key)[\w.-]*|id_(?:rsa|ed25519|ecdsa)|[\w.-]+\.key)\b/i],
|
|
369
|
+
];
|
|
370
|
+
for (const [reason, pattern] of checks) {
|
|
371
|
+
if (pattern.test(command)) return deny(reason, command);
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function inspectBashCommand(command, depth = 0, config = {}) {
|
|
377
|
+
if (depth === undefined) depth = 0;
|
|
378
|
+
if (depth > 8) return ask("unparseable", "nesting-depth", String(command || ""));
|
|
379
|
+
if (typeof command !== "string") return ask("unknown-format", "command is not a string", "non-string command");
|
|
380
|
+
if (command.trim() === "") return allow();
|
|
381
|
+
|
|
382
|
+
const tok = tokenize(command);
|
|
383
|
+
const tokens = tok.tokens || [];
|
|
384
|
+
|
|
385
|
+
if (!tok.ok) {
|
|
386
|
+
const knownDanger = inspectUnparsedDanger(command);
|
|
387
|
+
if (knownDanger) return knownDanger;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Pipeline inspection (#126)
|
|
391
|
+
const pipeCheck = inspectPipelines(tokens);
|
|
392
|
+
if (pipeCheck.dangerousPipeline) {
|
|
393
|
+
return deny(pipeCheck.reason, command);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const pipelineHeredocHit = inspectPipelineHeredocs(tokens, depth, config);
|
|
397
|
+
if (pipelineHeredocHit.deny || pipelineHeredocHit.ask) return pipelineHeredocHit;
|
|
398
|
+
|
|
399
|
+
// Silent error guard check (#90)
|
|
400
|
+
const silentHit = inspectSilentErrors(command);
|
|
401
|
+
|
|
402
|
+
const segs = splitSegments(tokens);
|
|
403
|
+
for (const seg of segs) {
|
|
404
|
+
const rawWords = wordsOf(seg);
|
|
405
|
+
const envHit = checkEnvExfiltration(rawWords, command);
|
|
406
|
+
if (envHit.deny) {
|
|
407
|
+
return deny("env-dump-leak", envHit.detail || command);
|
|
408
|
+
}
|
|
409
|
+
const wordTokens = wordTokensOf(seg);
|
|
410
|
+
const heredocs = seg.filter((t) => t.kind === "heredoc");
|
|
411
|
+
const argv = stripPrefixes(wordsOf(seg));
|
|
412
|
+
const commandToken = executableTokenOf(seg);
|
|
413
|
+
const commandName = commandNameFromSegment(seg);
|
|
414
|
+
|
|
415
|
+
// Silent error guard triggered on mutating system commands (#90)
|
|
416
|
+
if (silentHit.masked && (config.silentErrorGuard || DESTRUCTIVE_CMDS.has(commandName))) {
|
|
417
|
+
return ask("silent-error-masked", silentHit.detail, command);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for (const token of wordTokens) {
|
|
421
|
+
for (const substitution of token.substitutions || []) {
|
|
422
|
+
const nested = substitution.kind === "parameter-expansion"
|
|
423
|
+
? inspectEmbeddedExpansions(substitution.script, depth + 1, config)
|
|
424
|
+
: inspectBashCommand(substitution.script, depth + 1, config);
|
|
425
|
+
if (nested.deny || nested.ask) return nested;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Redirects security inspection (#126)
|
|
430
|
+
const redirects = extractRedirects(seg);
|
|
431
|
+
const redirectHit = inspectRedirectSecurity(redirects, {
|
|
432
|
+
workspaceDir: config.workspaceDir,
|
|
433
|
+
env: config.env,
|
|
434
|
+
});
|
|
435
|
+
if (redirectHit) {
|
|
436
|
+
return redirectHit.deny ? deny(redirectHit.reason, redirectHit.path) : ask(redirectHit.reason, redirectHit.detail, redirectHit.path);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (commandToken && commandToken.dynamic && !commandToken.value.includes("=")) {
|
|
440
|
+
return ask("dynamic-command", "expanded command name cannot be verified", commandToken.value);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const dynamicArgs = wordTokens.some((t) => (
|
|
444
|
+
t.dynamic && (!commandToken || t !== commandToken) && !t.value.includes("=")
|
|
445
|
+
));
|
|
446
|
+
if (dynamicArgs && ["rm", "find", "git", "systemctl", "service", "tee", "install", "cp", "mv", "truncate", "sed", "dd"].includes(commandName)) {
|
|
447
|
+
return ask("dynamic-arguments", "expanded command arguments cannot be verified", commandName);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (heredocs.length) {
|
|
451
|
+
for (const heredoc of heredocs) {
|
|
452
|
+
if (!heredoc.quoted) {
|
|
453
|
+
const expansionHit = inspectEmbeddedExpansions(heredoc.body, depth, config);
|
|
454
|
+
if (expansionHit.deny || expansionHit.ask) return expansionHit;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (SHELLS.has(commandName)) {
|
|
458
|
+
for (const heredoc of heredocs) {
|
|
459
|
+
const nested = inspectBashCommand(heredoc.body, depth + 1, config);
|
|
460
|
+
if (nested.deny || nested.ask) return nested;
|
|
461
|
+
}
|
|
462
|
+
} else if (INTERPRETERS.has(commandName)) {
|
|
463
|
+
for (const heredoc of heredocs) {
|
|
464
|
+
const nested = inspectInterpreterPayload(commandName, heredoc.body);
|
|
465
|
+
if (nested.deny || nested.ask) return nested;
|
|
466
|
+
}
|
|
467
|
+
} else if (DB_CLIENTS.has(commandName)) {
|
|
468
|
+
for (const heredoc of heredocs) {
|
|
469
|
+
if (DESTRUCTIVE_SQL.test(heredoc.body)) {
|
|
470
|
+
return deny("destructive-sql", heredoc.body);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const hit = inspectArgv(argv, depth, heredocs.length > 0, config);
|
|
477
|
+
if (hit.deny || hit.ask) return hit;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!tok.ok) return ask("unparseable", tok.reason || "unsupported shell syntax", tok.snippet || command);
|
|
481
|
+
return allow();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const PROMPT_INJECTION_PATTERNS = [
|
|
485
|
+
/\b(?:system\s+override\s*:\s*allow|ignore\s+(?:all\s+)?previous\s+instructions|you\s+are\s+now\s+(?:an?\s+)?unrestricted|admin\s+override\s*:\s*allow|bypass\s+all\s+(?:safety\s+)?checks|disregard\s+(?:all\s+)?safety\s+rules|developer\s+mode\s*:\s*enabled|disable\s+all\s+guardrails)\b/i,
|
|
486
|
+
/(?:\[system\]\s*:\s*approve|\[admin\]\s*:\s*allow|\bassistant\s*:\s*verdict\s*=\s*allow)\b/i,
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
export function computePayloadSize(args) {
|
|
490
|
+
if (args === null || args === undefined) return 0;
|
|
491
|
+
if (typeof args === "string") return Buffer.byteLength(args, "utf8");
|
|
492
|
+
try {
|
|
493
|
+
return Buffer.byteLength(JSON.stringify(args), "utf8");
|
|
494
|
+
} catch {
|
|
495
|
+
return 0;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function scanPromptInjection(args) {
|
|
500
|
+
if (!args) return null;
|
|
501
|
+
const list = [];
|
|
502
|
+
if (typeof args === "string") list.push(args);
|
|
503
|
+
else if (typeof args === "object") {
|
|
504
|
+
for (const key of ["command", "content", "script", "code", "input", "prompt", "patch", "text"]) {
|
|
505
|
+
if (typeof args[key] === "string") list.push(args[key]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const text of list) {
|
|
509
|
+
for (const pat of PROMPT_INJECTION_PATTERNS) {
|
|
510
|
+
const m = pat.exec(text);
|
|
511
|
+
if (m) return m[0];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function inspectExecution(execution, config) {
|
|
518
|
+
if (!config) config = {};
|
|
519
|
+
const toolName = config.toolName || DEFAULT_BASH_TOOL;
|
|
520
|
+
const fileTools = new Set(config.fileWriteTools || DEFAULT_FILE_WRITE_TOOLS);
|
|
521
|
+
const name = execution && execution.name;
|
|
522
|
+
const args = execution ? execution.arguments : undefined;
|
|
523
|
+
|
|
524
|
+
// 1. Argument payload size bound check (#71)
|
|
525
|
+
const maxPayload = typeof config.maxPayloadBytes === "number" ? config.maxPayloadBytes : 65536;
|
|
526
|
+
const payloadBytes = computePayloadSize(args);
|
|
527
|
+
if (payloadBytes > maxPayload) {
|
|
528
|
+
return ask("payload-too-large", `Payload size ${payloadBytes} bytes exceeds ${maxPayload} bytes limit`, `${payloadBytes} bytes`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 2. Anti-injection check (#72)
|
|
532
|
+
const injection = scanPromptInjection(args);
|
|
533
|
+
if (injection) {
|
|
534
|
+
return deny("reviewer-injection-attempt", `Prompt injection detected: ${injection}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 3. Reviewer agent read-only mode isolation (#65)
|
|
538
|
+
if (isReviewerAgent(execution, config)) {
|
|
539
|
+
if (name === toolName || fileTools.has(name)) {
|
|
540
|
+
return deny("reviewer-read-only", `Reviewer agent is strictly isolated in read-only mode; tool ${name} is prohibited`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (name === toolName) {
|
|
545
|
+
const command = args && typeof args === "object" ? args.command : undefined;
|
|
546
|
+
if (command === undefined || command === null || command === "") return allow();
|
|
547
|
+
if (typeof command !== "string") return ask("unknown-format", "command is not a string", "non-string command");
|
|
548
|
+
return inspectBashCommand(command, 0, config);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (typeof name === "string" && fileTools.has(name)) {
|
|
552
|
+
const path = extractFilePath(args);
|
|
553
|
+
if (!path) return ask("unknown-write-target", "file write target could not be identified", name);
|
|
554
|
+
// 4. Check tempRoots isolation (#102)
|
|
555
|
+
if (config.isolateTempRoots !== false) {
|
|
556
|
+
const tempIso = checkTempIsolation(path, config.workspaceDir);
|
|
557
|
+
if (!tempIso.isolated) {
|
|
558
|
+
return ask("unisolated-temp-path", tempIso.reason || "Target path targets global /tmp", path);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (config.workspaceDir) {
|
|
562
|
+
const safeCheck = resolveSafePath(path, config.workspaceDir);
|
|
563
|
+
if (!safeCheck.safe) return deny("path-traversal", safeCheck.reason || "path-traversal-escape", path);
|
|
564
|
+
if (isProtectedPath(safeCheck.canonical || path)) return deny("secret-write", path);
|
|
565
|
+
} else {
|
|
566
|
+
if (isPathTraversal(path, process.cwd())) {
|
|
567
|
+
return deny("path-traversal", "path-traversal-escape", path);
|
|
568
|
+
}
|
|
569
|
+
if (isProtectedPath(path)) return deny("secret-write", path);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return allow();
|
|
521
574
|
}
|