mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
fix(gateguard): classify standard launcher and GNU time options
Preserve contributor history and current-main behavior while resolving the exact reviewed follow-up.
Source-PR: https://github.com/affaan-m/ECC/pull/2829
Source-Parent: d811349224
Review-Manifest-SHA256: a7209685b2a5726777f7450a5983c28add8759654969b09ba3e4b803fcfaf8bd
This commit is contained in:
@@ -291,6 +291,9 @@ function tokenizeAllowlistedShellWords(input) {
|
||||
}
|
||||
|
||||
const SHELL_SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
|
||||
// Keep only the lexical information needed for Bash's reserved word `time`.
|
||||
// Quoted/escaped `time` is an external command, not a shell pipeline prefix.
|
||||
const SHELL_TIME_TOKENS = new WeakMap();
|
||||
|
||||
/**
|
||||
* Quote-aware split of a command line into segments, with quotes removed from
|
||||
@@ -307,20 +310,30 @@ const SHELL_SEGMENT_SEPARATORS = new Set([';', '|', '&', '\n', '\r']);
|
||||
function quoteAwareSegments(input) {
|
||||
const segments = [];
|
||||
let words = [];
|
||||
let timeTokens = new Set();
|
||||
let current = '';
|
||||
let hasWord = false;
|
||||
let literalWord = true;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
const flushWord = () => {
|
||||
if (hasWord) words.push(current);
|
||||
if (hasWord) {
|
||||
if (literalWord && ['time', '-p', '--'].includes(current)) timeTokens.add(words.length);
|
||||
words.push(current);
|
||||
}
|
||||
current = '';
|
||||
hasWord = false;
|
||||
literalWord = true;
|
||||
};
|
||||
const flushSegment = () => {
|
||||
flushWord();
|
||||
if (words.length) segments.push(words);
|
||||
if (words.length) {
|
||||
if (timeTokens.size) SHELL_TIME_TOKENS.set(words, timeTokens);
|
||||
segments.push(words);
|
||||
}
|
||||
words = [];
|
||||
timeTokens = new Set();
|
||||
};
|
||||
|
||||
const source = String(input || '');
|
||||
@@ -345,6 +358,7 @@ function quoteAwareSegments(input) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
literalWord = false;
|
||||
escaped = true;
|
||||
hasWord = true;
|
||||
continue;
|
||||
@@ -357,6 +371,7 @@ function quoteAwareSegments(input) {
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
literalWord = false;
|
||||
hasWord = true; // entering a quote starts a word, even if its content is empty
|
||||
continue;
|
||||
}
|
||||
@@ -532,6 +547,8 @@ function wrapperValueOption(arg, valueFlags) {
|
||||
|
||||
// Explicit external-launcher argv grammars for dd and shell-wrapper discovery.
|
||||
// Unknown flags do not justify guessing which later argument executes.
|
||||
// This literal allowlist cannot prove arbitrary custom-wrapper semantics or
|
||||
// resolve dynamically selected executables; quoted operand text stays data.
|
||||
const DD_LAUNCHER_OPTIONS = {
|
||||
xargs: {
|
||||
values: new Set(['-a', '--arg-file', '-d', '--delimiter', '-E', '-I', '-J', '-L', '-n', '--max-args', '-P', '--max-procs', '-s', '--max-chars', '--process-slot-var']),
|
||||
@@ -544,12 +561,38 @@ const DD_LAUNCHER_OPTIONS = {
|
||||
flags: new Set(['-v', '--verbose', '--foreground', '--preserve-status'])
|
||||
},
|
||||
nice: { values: new Set(['-n', '--adjustment']), optional: new Set(), flags: new Set() },
|
||||
nohup: { values: new Set(), optional: new Set(), flags: new Set() }
|
||||
nohup: { values: new Set(), optional: new Set(), flags: new Set() },
|
||||
time: {
|
||||
values: new Set(['-f', '--format', '-o', '--output-file']),
|
||||
optional: new Set(),
|
||||
flags: new Set(['-p', '--portability', '-a', '--append', '-q', '--quiet', '-v', '--verbose']),
|
||||
nonCommand: new Set(['--help', '-V', '--version'])
|
||||
},
|
||||
stdbuf: {
|
||||
values: new Set(['-i', '--input', '-o', '--output', '-e', '--error']),
|
||||
optional: new Set(), flags: new Set()
|
||||
},
|
||||
ionice: {
|
||||
values: new Set(['-c', '--class', '-n', '--classdata']),
|
||||
optional: new Set(), flags: new Set(['-t', '--ignore']),
|
||||
// These modes query/change existing processes rather than launch argv.
|
||||
nonCommand: new Set(['-p', '--pid', '-P', '--pgid', '-u', '--uid'])
|
||||
},
|
||||
setsid: {
|
||||
values: new Set(), optional: new Set(),
|
||||
flags: new Set(['-c', '--ctty', '-f', '--fork', '-w', '--wait'])
|
||||
}
|
||||
};
|
||||
|
||||
/** Return the command position after one explicitly supported launcher's options. */
|
||||
function ddLauncherCommandIndex(argv, index, name) {
|
||||
const { values, optional, flags } = DD_LAUNCHER_OPTIONS[name];
|
||||
const { values, optional, flags, nonCommand } = DD_LAUNCHER_OPTIONS[name];
|
||||
// GNU time uses getopt_long: unique prefixes resolve against its complete
|
||||
// eight-option table, including terminating help/version. Other launchers
|
||||
// retain their explicit spellings; this does not affect shell-keyword time.
|
||||
const timeLongOptions = name === 'time'
|
||||
? [...values, ...flags, ...nonCommand].filter(flag => flag.startsWith('--'))
|
||||
: null;
|
||||
index += 1;
|
||||
while (index < argv.length) {
|
||||
const arg = argv[index];
|
||||
@@ -565,7 +608,13 @@ function ddLauncherCommandIndex(argv, index, name) {
|
||||
}
|
||||
if (arg.startsWith('--')) {
|
||||
const separator = arg.indexOf('=');
|
||||
const flag = separator === -1 ? arg : arg.slice(0, separator);
|
||||
let flag = separator === -1 ? arg : arg.slice(0, separator);
|
||||
if (timeLongOptions && !timeLongOptions.includes(flag)) {
|
||||
const matches = timeLongOptions.filter(option => option.startsWith(flag));
|
||||
if (matches.length !== 1) return argv.length;
|
||||
[flag] = matches;
|
||||
}
|
||||
if (nonCommand && nonCommand.has(flag)) return argv.length;
|
||||
if (values.has(flag)) index += separator === -1 ? 2 : 1;
|
||||
else if (optional.has(flag) || (separator === -1 && flags.has(flag))) index += 1;
|
||||
else return argv.length;
|
||||
@@ -574,6 +623,7 @@ function ddLauncherCommandIndex(argv, index, name) {
|
||||
let consumesNext = false;
|
||||
for (let offset = 1; offset < arg.length; offset += 1) {
|
||||
const flag = `-${arg[offset]}`;
|
||||
if (nonCommand && nonCommand.has(flag)) return argv.length;
|
||||
if (values.has(flag)) {
|
||||
consumesNext = offset + 1 === arg.length;
|
||||
break;
|
||||
@@ -602,12 +652,27 @@ function unwrapLeadWrappers(tokens, allowShellBuiltins = true, allowDdLaunchers
|
||||
let argv = tokens.slice();
|
||||
let index = 0;
|
||||
let allowAssignments = true;
|
||||
let allowShellTime = allowShellBuiltins;
|
||||
const timeTokens = SHELL_TIME_TOKENS.get(tokens);
|
||||
let budget = tokens.reduce((size, token) => size + token.length + 1, 1);
|
||||
while (index < argv.length && budget-- > 0) {
|
||||
while (allowAssignments && index < argv.length && SHELL_ASSIGNMENT.test(argv[index])) index += 1;
|
||||
while (allowAssignments && index < argv.length && SHELL_ASSIGNMENT.test(argv[index])) {
|
||||
index += 1;
|
||||
allowShellTime = false;
|
||||
}
|
||||
if (index >= argv.length) return [];
|
||||
const base = commandBasename(argv[index]);
|
||||
if (allowDdLaunchers && allowShellTime && argv[index] === 'time' && timeTokens && timeTokens.has(index)) {
|
||||
// Current Bash accepts only raw -p and -- as reserved-time options.
|
||||
// Quotes/escapes make them executable words, unlike external time argv.
|
||||
// The next command/exec builtin or assignment keeps shell semantics.
|
||||
index += 1;
|
||||
if (argv[index] === '-p' && timeTokens.has(index)) index += 1;
|
||||
if (argv[index] === '--' && timeTokens.has(index)) index += 1;
|
||||
continue;
|
||||
}
|
||||
if (allowShellBuiltins && base === 'command') {
|
||||
allowShellTime = false;
|
||||
index += 1;
|
||||
while (index < argv.length && argv[index].startsWith('-') && argv[index] !== '-') {
|
||||
const flag = argv[index++];
|
||||
@@ -619,6 +684,7 @@ function unwrapLeadWrappers(tokens, allowShellBuiltins = true, allowDdLaunchers
|
||||
continue;
|
||||
}
|
||||
if (allowShellBuiltins && base === 'exec') {
|
||||
allowShellTime = false;
|
||||
index += 1;
|
||||
while (index < argv.length && argv[index].startsWith('-') && argv[index] !== '-') {
|
||||
const flag = argv[index];
|
||||
@@ -639,11 +705,13 @@ function unwrapLeadWrappers(tokens, allowShellBuiltins = true, allowDdLaunchers
|
||||
}
|
||||
if (allowDdLaunchers && Object.prototype.hasOwnProperty.call(DD_LAUNCHER_OPTIONS, base)) {
|
||||
index = ddLauncherCommandIndex(argv, index, base);
|
||||
allowShellTime = false;
|
||||
allowShellBuiltins = false;
|
||||
allowAssignments = false;
|
||||
continue;
|
||||
}
|
||||
if (base === 'sudo' || base === 'doas') {
|
||||
allowShellTime = false;
|
||||
allowShellBuiltins = false;
|
||||
allowAssignments = true;
|
||||
const valueFlags = base === 'sudo' ? SUDO_VALUE_FLAGS : DOAS_VALUE_FLAGS;
|
||||
@@ -661,6 +729,7 @@ function unwrapLeadWrappers(tokens, allowShellBuiltins = true, allowDdLaunchers
|
||||
continue;
|
||||
}
|
||||
if (base === 'env') {
|
||||
allowShellTime = false;
|
||||
allowShellBuiltins = false;
|
||||
allowAssignments = true;
|
||||
index += 1;
|
||||
|
||||
@@ -184,6 +184,82 @@ function runDdRegressionTests() {
|
||||
else failed++;
|
||||
};
|
||||
const destructive = [
|
||||
// GNU external time option names, prefixes and values stay distinct.
|
||||
"/usr/bin/time -q dd of=output",
|
||||
"/usr/bin/time --quiet dd if=input",
|
||||
"/usr/bin/time --output-file report dd of=output",
|
||||
"/usr/bin/time --output-file=report dd of=output",
|
||||
"/usr/bin/time --output-file=dd dd of=output",
|
||||
"/usr/bin/time --q dd of=output",
|
||||
"/usr/bin/time --qui dd if=input",
|
||||
"/usr/bin/time --output-f=report dd of=output",
|
||||
"/usr/bin/time --o dd dd of=output",
|
||||
"/usr/bin/time --a --f dd --o report --p --q --verb dd of=output",
|
||||
"/usr/bin/time -qfFORMAT dd of=output",
|
||||
"/usr/bin/time -apqvfdd dd if=input",
|
||||
"/usr/bin/time -qo dd dd of=output",
|
||||
"/usr/bin/time --quiet -- dd of=output",
|
||||
"/usr/bin/time --format= --output-file=report --append --portability --quiet --verbose dd of=output",
|
||||
"'time' '--quiet' dd of=output",
|
||||
"command time --q dd of=output",
|
||||
"env time --output-f=report dd of=output",
|
||||
"sudo time -q dd of=output",
|
||||
"find . -exec time --q dd of=output \\;",
|
||||
"timeout 2 /usr/bin/time --q sh -c 'dd of=output'",
|
||||
"/usr/bin/time --output-file='dd of=output' sh -c 'dd if=input'",
|
||||
"/usr/bin/time --f='dd of=output' stdbuf -oL dd of=output",
|
||||
"sh -c '\"time\" --q dd of=output'",
|
||||
"time -- /usr/bin/time --q dd of=output",
|
||||
// Current Bash reserved-time syntax keeps raw option identity.
|
||||
"time -- dd of=output",
|
||||
"time -p -- dd of=output",
|
||||
"time -- command -p dd of=output",
|
||||
"time -p -- exec dd if=input",
|
||||
"time -- A=1 dd of=output",
|
||||
"time -p -- sh -c 'dd of=output'",
|
||||
"time -p -- time -- dd of=output",
|
||||
"'time' '-p' '--' dd of=output",
|
||||
"env time -p -- dd of=output",
|
||||
"time -\\\np -- dd of=output",
|
||||
// Literal launcher argv cases; these strings are never executed.
|
||||
"time dd if=input",
|
||||
"time -p dd of=output",
|
||||
"time command -p dd if=input",
|
||||
"time -p exec dd of=output",
|
||||
"time A=1 dd of=output",
|
||||
"/usr/bin/time dd if=input",
|
||||
"/usr/bin/time -f dd dd of=output",
|
||||
"/usr/bin/time -o dd -apv dd of=output",
|
||||
"/usr/bin/time --format=dd --output=dd --append --portability --verbose dd of=output",
|
||||
"\"time\" -f \"%e\" dd of=output",
|
||||
"'time' -o report dd if=input",
|
||||
"\\time -f dd dd of=output",
|
||||
"command time -f dd dd of=output",
|
||||
"env time -f dd dd of=output",
|
||||
"sudo time -p dd of=output",
|
||||
"find . -exec time -f dd dd of=output \\;",
|
||||
"time -p sh -c 'dd of=output'",
|
||||
"'time' -f dd sh -c 'dd if=input'",
|
||||
"stdbuf -i0 -oL -e0 dd of=output",
|
||||
"stdbuf --input=0 --output=L --error=0 dd if=input",
|
||||
"stdbuf -o L -- dd if=input",
|
||||
"ionice dd of=output",
|
||||
"ionice -c 2 -n 7 -t dd if=input",
|
||||
"ionice -tc2 -n7 dd of=output",
|
||||
"ionice --class=idle --classdata=7 --ignore dd of=output",
|
||||
"ionice -- dd if=input",
|
||||
"setsid dd of=output",
|
||||
"setsid -cfw dd if=input",
|
||||
"setsid --ctty --fork --wait -- dd of=output",
|
||||
"stdbuf -oL sh -c 'dd of=output'",
|
||||
"ionice -c2 sh -c 'dd of=output'",
|
||||
"setsid -w sh -c 'dd of=output'",
|
||||
"time -p stdbuf -oL ionice -c2 setsid -f env -S 'dd of=output'",
|
||||
"sudo -u root stdbuf -oL ionice -c2 setsid dd of=output",
|
||||
"find . -exec stdbuf -oL setsid dd of=output \\;",
|
||||
"find . -exec ionice -c2 setsid dd if=input \\;",
|
||||
"sh -c 'time -p stdbuf -oL dd of=output'",
|
||||
"setsid sh -c 'cat <<EOF\n$(dd of=output)\nEOF'",
|
||||
'dd if=/dev/zero of=/dev/sda',
|
||||
'dd of=/dev/sda bs=1M',
|
||||
'cat /dev/zero | dd of=/dev/sda',
|
||||
@@ -306,6 +382,118 @@ function runDdRegressionTests() {
|
||||
"find . -exec 'rm' {} \\;"
|
||||
];
|
||||
const passive = [
|
||||
// GNU external time option names, prefixes and values stay distinct.
|
||||
"/usr/bin/time --v dd of=output",
|
||||
"/usr/bin/time --ver dd of=output",
|
||||
"/usr/bin/time --quiet=value dd of=output",
|
||||
"/usr/bin/time --q=value dd of=output",
|
||||
"/usr/bin/time --append=dd dd of=output",
|
||||
"/usr/bin/time --portability=dd dd of=output",
|
||||
"/usr/bin/time --verbose=dd dd of=output",
|
||||
"/usr/bin/time --help dd of=output",
|
||||
"/usr/bin/time --h dd of=output",
|
||||
"/usr/bin/time --he dd of=output",
|
||||
"/usr/bin/time --version dd of=output",
|
||||
"/usr/bin/time --vers dd of=output",
|
||||
"/usr/bin/time -qV dd of=output",
|
||||
"/usr/bin/time --q --help dd of=output",
|
||||
"/usr/bin/time --output-file dd echo of=output",
|
||||
"/usr/bin/time --output-file=dd echo of=output",
|
||||
"/usr/bin/time --output-f=dd echo of=output",
|
||||
"/usr/bin/time --o dd echo of=output",
|
||||
"/usr/bin/time -qfdd echo of=output",
|
||||
"/usr/bin/time --f=dd echo of=output",
|
||||
"/usr/bin/time --output-file dd of=output",
|
||||
"/usr/bin/time --output-file",
|
||||
"/usr/bin/time --quiet --output-file",
|
||||
"/usr/bin/time --unknown dd of=output",
|
||||
"/usr/bin/time --quieter dd of=output",
|
||||
"/usr/bin/time --=dd dd of=output",
|
||||
"/usr/bin/time -- -q dd of=output",
|
||||
"/usr/bin/time echo --quiet dd of=output",
|
||||
"/usr/bin/time --q command dd of=output",
|
||||
"/usr/bin/time --q echo 'dd of=output'",
|
||||
"/usr/bin/time --output-file='sh -c dd of=output' echo safe",
|
||||
"/usr/bin/time -q sh -c 'echo \"dd of=output\"'",
|
||||
"echo '/usr/bin/time --q dd of=output'",
|
||||
"find . -name 'time --q dd of=output' -print",
|
||||
"find . -exec echo time --q dd of=output \\;",
|
||||
"time -q dd of=output",
|
||||
"time --quiet dd of=output",
|
||||
"time --output-file=report dd of=output",
|
||||
// Current Bash reserved-time syntax keeps raw option identity.
|
||||
"time '-p' dd of=output",
|
||||
"time \"-p\" dd of=output",
|
||||
"time \\-p dd of=output",
|
||||
"time -\\p dd of=output",
|
||||
"time '--' dd of=output",
|
||||
"time \"--\" dd of=output",
|
||||
"time \\-- dd of=output",
|
||||
"time -p '--' dd of=output",
|
||||
"time -p \\-- dd of=output",
|
||||
"time -- -p dd of=output",
|
||||
"time -p -p dd of=output",
|
||||
"time -p'' dd of=output",
|
||||
"time --'' dd of=output",
|
||||
"time -- command -v dd of=output",
|
||||
"time -- echo 'dd of=output'",
|
||||
// Literal launcher argv cases; these strings are never executed.
|
||||
"time echo dd if=input",
|
||||
"time -p command -v dd if=input",
|
||||
"time command -pV dd of=output",
|
||||
"time -p exec -a dd echo of=output",
|
||||
"time -f dd of=output",
|
||||
"/usr/bin/time -f dd echo of=output",
|
||||
"/usr/bin/time --format=dd echo if=input",
|
||||
"/usr/bin/time -o dd echo of=output",
|
||||
"/usr/bin/time --output=dd echo if=input",
|
||||
"/usr/bin/time -afdd echo if=input",
|
||||
"/usr/bin/time --help dd of=output",
|
||||
"/usr/bin/time --version dd if=input",
|
||||
"'time' -f dd echo of=output",
|
||||
"\\time -o dd echo if=input",
|
||||
"command time -f dd echo if=input",
|
||||
"env time command dd of=output",
|
||||
"A=1 time command dd of=output",
|
||||
"/usr/bin/time command dd of=output",
|
||||
"'time' command dd of=output",
|
||||
"stdbuf -o dd echo if=input",
|
||||
"stdbuf --input=dd echo of=output",
|
||||
"stdbuf -edd echo of=output",
|
||||
"stdbuf --help dd if=input",
|
||||
"stdbuf --version dd of=output",
|
||||
"stdbuf -oL echo 'dd of=output'",
|
||||
"stdbuf -oL command dd if=input",
|
||||
"ionice -c dd echo of=output",
|
||||
"ionice --classdata=dd echo if=input",
|
||||
"ionice -p 1 dd of=output",
|
||||
"ionice -p1 dd if=input",
|
||||
"ionice --pid=1 dd of=output",
|
||||
"ionice -P 1 dd if=input",
|
||||
"ionice --pgid 1 dd of=output",
|
||||
"ionice -u 1 dd if=input",
|
||||
"ionice --uid=1 dd of=output",
|
||||
"ionice -tc2 -p1 dd of=output",
|
||||
"ionice -h dd if=input",
|
||||
"ionice --help dd of=output",
|
||||
"ionice -V dd of=output",
|
||||
"ionice --version dd if=input",
|
||||
"ionice -c2 echo 'dd if=input'",
|
||||
"ionice -c2 command dd of=output",
|
||||
"setsid -h dd if=input",
|
||||
"setsid --help dd of=output",
|
||||
"setsid -V dd of=output",
|
||||
"setsid --version dd if=input",
|
||||
"setsid -w echo dd of=output",
|
||||
"setsid command dd if=input",
|
||||
"time -p stdbuf -oL ionice -c2 setsid echo 'dd of=output'",
|
||||
"setsid -w sh -c 'echo \"dd of=output\"'",
|
||||
"time -p sh -c 'cat <<EOF\ndd of=output\nEOF'",
|
||||
"echo 'time dd if=input; setsid dd of=output'",
|
||||
"printf '%s' 'stdbuf -oL dd if=input'",
|
||||
"find . -name \"time -p dd of=output\" -print",
|
||||
"find . -exec echo setsid dd of=output \\;",
|
||||
"env -S 'echo time dd if=input; setsid dd of=output'",
|
||||
'echo dd if=input',
|
||||
'echo dd of=/dev/sda',
|
||||
'grep dd of=output file',
|
||||
|
||||
Reference in New Issue
Block a user