From bbe5d784201b9b67dca0ad092f673e2b5b4db046 Mon Sep 17 00:00:00 2001 From: L4XB Date: Thu, 24 Sep 2026 22:39:44 +0200 Subject: [PATCH] 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. --- scripts/hooks/gateguard-fact-force.js | 24 ++++++++++++++++++++++++ tests/hooks/gateguard-fact-force.test.js | 8 ++++++++ 2 files changed, 32 insertions(+) diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js index 48a9d466d..d7106c6dd 100644 --- a/scripts/hooks/gateguard-fact-force.js +++ b/scripts/hooks/gateguard-fact-force.js @@ -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; } diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index dd8488085..8dddc21c5 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -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'); })