mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-10 10:06:00 +00:00
fix(hooks): end echo output with its newline when group writes are joined
echo writes a newline unless -n is given, so { echo 'git pu'; echo 'sh --no-verify'; } | sh feeds sh two separate lines. The joined read glued them into git push --no-verify and blocked the group. echoText() and printf's %s\n branch now end their text with the newline they write, while echo -n and printf without one still join directly.
This commit is contained in:
2 files changed
+12
-2
No files matched your search
@@ -833,9 +833,18 @@ function groupSources(scope, budget) {
|
||||
// $'...' in shell-scan, only word boundaries are decoded, not numeric escapes.
|
||||
const PRINTF_SPACE_ESCAPES = { n: '\n', t: '\t', r: '\r', v: '\v', f: '\f', '\\': '\\' };
|
||||
|
||||
// echo ends its output with a newline unless an -n option asks it not to, so
|
||||
// two echoes in a group write two lines, not one joined command.
|
||||
function echoText(words) {
|
||||
const options = words.slice(1).filter(word => /^-[neE]+$/.test(word.value));
|
||||
const text = words.slice(1).filter(word => !options.includes(word)).map(word => word.value).join(' ');
|
||||
return options.some(word => word.value.includes('n')) ? text : `${text}\n`;
|
||||
}
|
||||
|
||||
function printfText(words) {
|
||||
const format = words[1]?.value || '';
|
||||
if (format === '%s' || format === '%s\\n') return words.slice(2).map(word => word.value).join('\n');
|
||||
if (format === '%s') return words.slice(2).map(word => word.value).join('\n');
|
||||
if (format === '%s\\n') return words.slice(2).map(word => `${word.value}\n`).join('');
|
||||
const decoded = format.replace(/\\([ntrvf\\])/g, (escape, c) => PRINTF_SPACE_ESCAPES[c]);
|
||||
return [decoded, ...words.slice(2).map(word => word.value)].join(' ');
|
||||
}
|
||||
@@ -850,7 +859,7 @@ function pipelineSources(command, budget) {
|
||||
const { words } = executableWords(current.words, budget);
|
||||
for (const word of words) budget.spend(word.value.length + 3);
|
||||
const name = basename(words[0]?.value || '');
|
||||
if (name === 'echo') sources.push({ text: words.slice(1).filter(word => !/^-[neE]+$/.test(word.value)).map(word => word.value).join(' ') });
|
||||
if (name === 'echo') sources.push({ text: echoText(words) });
|
||||
if (name === 'printf' && words[1]?.value !== '-v') sources.push({ text: printfText(words) });
|
||||
if (!DATA_COMMANDS.has(name)) {
|
||||
// Foreign transformations can introduce literal bypasses into executable
|
||||
|
||||
@@ -284,6 +284,7 @@ const nonLeakingPayloads = [
|
||||
['allows the distinct executable with a literal escaped dot', '"git""\\.exe" commit -n -m x'],
|
||||
// A complete echo operand is data; accepted role repair intentionally corrects the authored broad-blocking expectation.
|
||||
['allows quoted echo data (corrected author expectation)', 'echo "git commit -n"'],
|
||||
['allows two echo lines in a group piped to sh', "{ echo 'git pu'; echo 'sh --no-verify'; } | sh"],
|
||||
['python heredoc string with later bash -n', 'python3 - <<\'PY\'\nold="git add -A\\nif ! git diff --cached --quiet; then\\n git commit -q -m \\"vault sync"\nPY\nbash -n vault-sync.sh'],
|
||||
['assignment string with later bash -n', 'old="git commit -q -m x"; bash -n x.sh'],
|
||||
['plain commit followed by later-line bash -n', 'git commit -m x\nbash -n s.sh'],
|
||||
|
||||
Reference in new issue
Block a user