fix(gateguard): detect stdin dd writes and launcher-wrapped shells

This commit is contained in:
affaan-m
2026-09-27 22:25:29 -04:00
parent a74c743370
commit d811349224
2 changed files with 31 additions and 5 deletions
+5 -4
View File
@@ -530,7 +530,7 @@ function wrapperValueOption(arg, valueFlags) {
return null;
}
// Explicit external-launcher argv grammars used only by the dd classifier.
// Explicit external-launcher argv grammars for dd and shell-wrapper discovery.
// Unknown flags do not justify guessing which later argument executes.
const DD_LAUNCHER_OPTIONS = {
xargs: {
@@ -735,7 +735,7 @@ function isDestructiveQuoteAware(raw, depth = 0) {
if (isDestructiveDd(tokens)) return true;
if (isDestructiveSqlClient(tokens)) return true;
if (isDestructiveFindExec(tokens)) return true;
const argv = unwrapLeadWrappers(tokens);
const argv = unwrapLeadWrappers(tokens, true, true);
if (SHELL_WRAPPERS.has(commandBasename(argv[0]))) {
const ci = argv.indexOf('-c', 1);
if (ci !== -1 && argv[ci + 1] && isDestructiveQuoteAware(argv[ci + 1], depth + 1)) {
@@ -763,7 +763,8 @@ function commandBasename(token) {
}
/**
* Detect a `dd` invocation carrying an `if=` operand.
* Detect a `dd` invocation carrying an `if=` or `of=` operand.
* Keep the existing input-file gate and include output-only writes from stdin.
*
* Token-based rather than a regex arm because the verdict has to depend on
* `dd` being the command, not on `dd if=` appearing anywhere in the line:
@@ -779,7 +780,7 @@ function commandBasename(token) {
*/
function isDestructiveDd(tokens, allowShellBuiltins = true) {
const argv = unwrapLeadWrappers(tokens, allowShellBuiltins, true);
return commandBasename(argv[0]) === 'dd' && argv.slice(1).some(operand => /^if=/i.test(operand));
return commandBasename(argv[0]) === 'dd' && argv.slice(1).some(operand => /^(?:if|of)=/i.test(operand));
}
/**
+26 -1
View File
@@ -185,6 +185,18 @@ function runDdRegressionTests() {
};
const destructive = [
'dd if=/dev/zero of=/dev/sda',
'dd of=/dev/sda bs=1M',
'cat /dev/zero | dd of=/dev/sda',
'sudo dd of=/dev/sda < /dev/zero',
'dd bs=1M of="./output"',
"timeout 60 bash -c 'dd if=/dev/zero of=/dev/sda'",
"nohup sh -c 'dd if=input'",
"nice -n 5 sh -c 'dd if=input'",
"xargs sh -c 'dd if=input'",
"timeout 2 sh -c 'dd of=output'",
"nohup sh -c 'echo $(dd of=output)'",
"nice -n 5 sh -c 'cat <<EOF\n$(dd of=output)\nEOF'",
'find . -exec dd of=output \\;',
'dd if=./image of=./out',
'dd of=./out bs=1M if="./image"',
"'/bin/dd' if=input of=output",
@@ -295,6 +307,16 @@ function runDdRegressionTests() {
];
const passive = [
'echo dd if=input',
'echo dd of=/dev/sda',
'grep dd of=output file',
"printf '%s' 'dd of=output'",
'dd count=0',
"timeout 2 echo 'sh -c dd of=output'",
"timeout 2 sh -c 'echo \"dd of=output\"'",
"nohup sh -c 'cat <<EOF\ndd of=output\nEOF'",
"nice -n 5 echo 'dd of=output'",
"xargs echo 'sh -c dd of=output'",
'echo "note; find . -exec dd of=output \\;"',
'command -v dd',
'command -v dd if=input',
'command -V dd if=input',
@@ -410,7 +432,9 @@ function runDdRegressionTests() {
['sudo -u dd echo if=input', false],
["env -S 'echo ok; dd if=input'", false],
['find . -exec echo {} \\; -exec dd if=input \\;', true],
['command -pv dd', false]
['command -pv dd', false],
["timeout 2 sh -c 'dd if=input'", true],
['cat /dev/zero | dd of=/dev/sda', true]
]) {
check(`dd hook-input contract: ${command}`, () => {
fs.rmSync(stateDir, { recursive: true, force: true });
@@ -617,6 +641,7 @@ function runTests() {
if (
test(`denies dd whose input path is not word-initial: ${command}`, () => {
const result = runBashHook({ tool_name: 'Bash', tool_input: { command } });
assert.strictEqual(result.code, 0, `hook should exit successfully for ${command}`);
const output = parseOutput(result.stdout);
assert.ok(output, 'hook should produce JSON output');
assert.ok(output.hookSpecificOutput, 'hook should return a permission decision');