fix(gateguard): check every command su is given, not only the first

`su postgres -c "echo ok" -c "psql -c 'drop table users'"` runs the last
-c, but only the first was scanned, so the drop went through. Every
-c/--command value is now scanned as a command line of its own.
This commit is contained in:
L4XB committed 2026-09-24 23:11:54 +02:00
1 parent bbe5d78420
commit 2df1aa7a92
2 files changed
+20 -11

No files matched your search

+18 -11
View File
@@ -360,22 +360,28 @@ function quoteAwareSegments(input) {
const SHELL_WRAPPERS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh']);
/**
* The command line `su` runs through the target user's shell: the value of
* `-c`/`--command`, or of a short-option cluster ending in `c` (`-lc`). `su`
* passes the arguments after `--` to that shell, which runs a `-c` there too,
* so the scan does not stop at `--`.
* The command lines `su` may run through the target user's shell: the value of
* every `-c`/`--command`, or of a short-option cluster ending in `c` (`-lc`).
* `su` runs only the last one, so each is checked rather than guessing which
* wins. `su` passes the arguments after `--` to that shell, which runs a `-c`
* there too, so the scan does not stop at `--`.
*
* @param {string[]} tokens dequoted tokens for one segment
* @param {number} start index of the `su` token
* @returns {string|null}
* @returns {string[]}
*/
function suCommandLine(tokens, start) {
function suCommandLines(tokens, start) {
const commandLines = [];
for (let i = start + 1; i < tokens.length; i += 1) {
const arg = tokens[i];
if (arg.startsWith('--command=')) return arg.slice('--command='.length);
if (arg === '--command' || /^-[A-Za-z]*c$/.test(arg)) return i + 1 < tokens.length ? tokens[i + 1] : null;
if (arg.startsWith('--command=')) {
commandLines.push(arg.slice('--command='.length));
} else if ((arg === '--command' || /^-[A-Za-z]*c$/.test(arg)) && i + 1 < tokens.length) {
commandLines.push(tokens[i + 1]);
i += 1;
}
}
return null;
return commandLines;
}
/**
@@ -637,8 +643,9 @@ function isDestructiveQuoteAware(raw, depth = 0) {
}
}
if (base === 'su') {
const commandLine = suCommandLine(tokens, wi);
if (commandLine && isDestructiveQuoteAware(commandLine, depth + 1)) return true;
for (const commandLine of suCommandLines(tokens, wi)) {
if (isDestructiveQuoteAware(commandLine, depth + 1)) return true;
}
}
}
return false;
+2
View File
@@ -1556,6 +1556,8 @@ function runTests() {
expectDestructiveDeny('su -lc "psql -c \'drop table users\'" postgres', 'su -lc psql');
expectDestructiveDeny('su --command="psql -c \'drop table users\'" postgres', 'su --command= psql');
expectDestructiveDeny('su postgres -- -c "psql -c \'drop table users\'"', 'su -c after --');
expectDestructiveDeny('su postgres -c "echo ok" -c "psql -c \'drop table users\'"', 'su runs its last -c');
expectDestructiveDeny('su -c "psql -c \'drop table users\'" -c "echo ok" postgres', 'su -c with a later harmless -c');
expectDestructiveDeny('taskset -c 0 psql -c "drop table users"', 'taskset -c psql');
})
)