fix(gateguard): read the command line su runs, and unwrap taskset

`su postgres -c "psql -c 'drop table users'"` resolved to `su`, so the
SQL-client check never saw psql. The value of su's -c/--command (or of a
short-option cluster ending in c, as in -lc) is now scanned as a command
line of its own, like the value of sh -c. The scan continues past `--`,
since su hands the arguments after it to the target shell, which runs a
-c there too.

taskset joins the runners: its -c is a flag and the CPU mask is one
positional, so `taskset -c 0 psql ...` resolves to psql.
This commit is contained in:
L4XB committed 2026-09-24 22:39:44 +02:00
1 parent 64e004a962
commit bbe5d78420
2 files changed
+32

No files matched your search

+24
View File
@@ -359,6 +359,25 @@ 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 `--`.
*
* @param {string[]} tokens dequoted tokens for one segment
* @param {number} start index of the `su` token
* @returns {string|null}
*/
function suCommandLine(tokens, start) {
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;
}
return null;
}
/**
* SQL clients whose `-c`/`-e`/positional arguments carry SQL statements.
* Quoted SQL (e.g. `psql -c "drop table users"`) is invisible to the
@@ -432,6 +451,7 @@ const RUNNER_PREFIXES = {
nohup: {},
setsid: {},
stdbuf: { valueOptions: ['-i', '-o', '-e', '--input', '--output', '--error'] },
taskset: { positionals: 1 },
time: { valueOptions: ['-f', '--format', '-o', '--output'] },
timeout: { valueOptions: ['-s', '--signal', '-k', '--kill-after'], positionals: 1 }
};
@@ -616,6 +636,10 @@ function isDestructiveQuoteAware(raw, depth = 0) {
return true;
}
}
if (base === 'su') {
const commandLine = suCommandLine(tokens, wi);
if (commandLine && isDestructiveQuoteAware(commandLine, depth + 1)) return true;
}
}
return false;
}
+8
View File
@@ -1551,6 +1551,12 @@ function runTests() {
expectDestructiveDeny('timeout 30 psql -c "drop table users"', 'timeout psql');
expectDestructiveDeny('nohup mysql -e "delete from sessions"', 'nohup mysql');
expectDestructiveDeny('sudo nice -n 5 timeout -s KILL 30 psql -c "drop table users"', 'stacked runners psql');
expectDestructiveDeny('sudo su postgres -c "psql -c \'drop table users\'"', 'sudo su -c psql');
expectDestructiveDeny('su - postgres -c "psql -c \'drop table users\'"', 'su - user -c psql');
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('taskset -c 0 psql -c "drop table users"', 'taskset -c psql');
})
)
passed++;
@@ -1573,6 +1579,8 @@ function runTests() {
expectAllow('bash -lc "psql -c \'select count(*) from users\'"', 'bash -lc select');
expectAllow('nice -n 10 psql -c "select 1"', 'nice select');
expectAllow('timeout 30 git status', 'timeout git status');
expectAllow('su postgres -c "psql -c \'select 1\'"', 'su -c select');
expectAllow('taskset -c 0 git status', 'taskset git status');
expectAllow('env -S \'psql -c "select 1"\'', 'env -S select');
expectAllow('env -S \'echo\' \'; psql -c "drop table users"\'', 'env -S echo with a literal argument');
})