mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-15 21:33:04 +08:00
fix(hooks): close grouped command bypasses in gateguard (#1912)
Inspect executable bodies inside plain subshells and brace groups before applying destructive command classifiers.\n\nCo-authored-by: Jamkris <82251632+Jamkris@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,11 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
extractCommandSubstitutions,
|
||||
extractSubshellGroups,
|
||||
extractBraceGroups
|
||||
} = require('../lib/shell-substitution');
|
||||
|
||||
// Session state — scoped per session to avoid cross-session races.
|
||||
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
|
||||
@@ -84,105 +89,6 @@ function explodeSubshells(input) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract executable command-substitution bodies from a shell line. Single
|
||||
* quotes are literal, so substitutions inside them are ignored; double quotes
|
||||
* still permit substitutions, so those bodies are scanned before quoted text
|
||||
* is stripped.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function extractCommandSubstitutions(input) {
|
||||
const source = String(input || '');
|
||||
const substitutions = [];
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
const prev = source[i - 1];
|
||||
|
||||
if (ch === '\\' && !inSingle) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '`') {
|
||||
let body = '';
|
||||
i += 1;
|
||||
while (i < source.length) {
|
||||
const inner = source[i];
|
||||
if (inner === '\\') {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === '`') {
|
||||
break;
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '$' && source[i + 1] === '(') {
|
||||
let depth = 1;
|
||||
let body = '';
|
||||
i += 2;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
if (inner === '\\') {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === '(') {
|
||||
depth += 1;
|
||||
} else if (inner === ')') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return substitutions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a command line into top-level segments at unquoted shell
|
||||
* separators (`;`, `|`, `&`, `&&`, `||`) and across subshells
|
||||
@@ -392,6 +298,54 @@ function isDestructiveGit(tokens) {
|
||||
* @param {string} command
|
||||
* @returns {boolean}
|
||||
*/
|
||||
/**
|
||||
* Walk every executable body reachable from a raw command line and
|
||||
* return them as a flat list. Bodies that bash will execute live in
|
||||
* three different syntactic constructs, each handled by a sibling
|
||||
* extractor in `scripts/lib/shell-substitution.js`:
|
||||
* - `$(...)` and backticks via `extractCommandSubstitutions`
|
||||
* - plain `(...)` subshells via `extractSubshellGroups`
|
||||
* - `{ ...; }` brace groups via `extractBraceGroups`
|
||||
*
|
||||
* Each extractor recurses into its own syntax. The BFS here adds
|
||||
* cross-syntax discovery — e.g. a `(...)` inside a `$(...)` body, or
|
||||
* a `{ ...; }` inside a `(...)` body — by feeding every harvested
|
||||
* body back through all three extractors. A `seen` set bounds the
|
||||
* cost to O(unique bodies).
|
||||
*
|
||||
* @param {string} raw
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function collectExecutableBodies(raw) {
|
||||
const bodies = [raw];
|
||||
const queue = [raw];
|
||||
const seen = new Set();
|
||||
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (seen.has(current)) continue;
|
||||
seen.add(current);
|
||||
|
||||
for (const body of extractCommandSubstitutions(current)) {
|
||||
if (seen.has(body)) continue;
|
||||
bodies.push(body);
|
||||
queue.push(body);
|
||||
}
|
||||
for (const body of extractSubshellGroups(current)) {
|
||||
if (seen.has(body)) continue;
|
||||
bodies.push(body);
|
||||
queue.push(body);
|
||||
}
|
||||
for (const body of extractBraceGroups(current)) {
|
||||
if (seen.has(body)) continue;
|
||||
bodies.push(body);
|
||||
queue.push(body);
|
||||
}
|
||||
}
|
||||
|
||||
return bodies;
|
||||
}
|
||||
|
||||
function isDestructiveBash(command) {
|
||||
// The SQL/dd phrases live in command bodies, not as flag-bearing
|
||||
// arguments, so we still match them by regex — but on the input
|
||||
@@ -401,7 +355,7 @@ function isDestructiveBash(command) {
|
||||
const flattened = explodeSubshells(stripQuotedStrings(raw));
|
||||
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
|
||||
|
||||
const segments = [raw, ...extractCommandSubstitutions(raw)].flatMap(splitCommandSegments);
|
||||
const segments = collectExecutableBodies(raw).flatMap(splitCommandSegments);
|
||||
for (const segment of segments) {
|
||||
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
|
||||
const tokens = tokenize(segment);
|
||||
|
||||
Reference in New Issue
Block a user