test(fleet): harden wrapped command validation
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -135,12 +135,36 @@ function commandBasename(token: string): string {
|
|||||||
return token.slice(token.lastIndexOf('/') + 1);
|
return token.slice(token.lastIndexOf('/') + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellCommandTokens(source: string): string[] {
|
||||||
|
const tokens: string[] = [];
|
||||||
|
let token = '';
|
||||||
|
let quote = '';
|
||||||
|
|
||||||
|
for (const character of source) {
|
||||||
|
if (quote !== '') {
|
||||||
|
if (character === quote) quote = '';
|
||||||
|
else token += character;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === "'" || character === '"') {
|
||||||
|
quote = character;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/\s/.test(character)) {
|
||||||
|
if (token !== '') {
|
||||||
|
tokens.push(token);
|
||||||
|
token = '';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
token += character;
|
||||||
|
}
|
||||||
|
if (token !== '') tokens.push(token);
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizedCommandTokens(segment: string): string[] {
|
function normalizedCommandTokens(segment: string): string[] {
|
||||||
const tokens = segment
|
const tokens = shellCommandTokens(segment.trim().replace(/^(?:[$#>]\s*)/, ''));
|
||||||
.trim()
|
|
||||||
.replace(/^(?:[$#>]\s*)/, '')
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter((token): boolean => token !== '');
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
while (index < tokens.length) {
|
while (index < tokens.length) {
|
||||||
@@ -162,7 +186,13 @@ function normalizedCommandTokens(segment: string): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (/^(?:-S|--split-string)$/.test(token)) {
|
if (/^(?:-S|--split-string)$/.test(token)) {
|
||||||
index += 1;
|
const splitTokens = shellCommandTokens(tokens[index + 1] ?? '');
|
||||||
|
tokens.splice(index, 2, ...splitTokens);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const splitString = token.match(/^(?:-S|--split-string)=(.*)$/)?.[1];
|
||||||
|
if (splitString !== undefined) {
|
||||||
|
tokens.splice(index, 1, ...shellCommandTokens(splitString));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (token.startsWith('-')) {
|
if (token.startsWith('-')) {
|
||||||
@@ -175,7 +205,14 @@ function normalizedCommandTokens(segment: string): string[] {
|
|||||||
}
|
}
|
||||||
if (wrapper === 'command') {
|
if (wrapper === 'command') {
|
||||||
index += 1;
|
index += 1;
|
||||||
while ((tokens[index] ?? '').startsWith('-')) index += 1;
|
let availabilityQuery = false;
|
||||||
|
while ((tokens[index] ?? '').startsWith('-')) {
|
||||||
|
const option = tokens[index] ?? '';
|
||||||
|
index += 1;
|
||||||
|
if (option === '--') break;
|
||||||
|
if (/^-[^-]*[vV]/.test(option)) availabilityQuery = true;
|
||||||
|
}
|
||||||
|
if (availabilityQuery) return [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -226,10 +263,28 @@ function containsPrivilegedCommand(source: string): boolean {
|
|||||||
const command = commandBasename(tokens[0] ?? '');
|
const command = commandBasename(tokens[0] ?? '');
|
||||||
if (privilegedCommands.has(command)) return true;
|
if (privilegedCommands.has(command)) return true;
|
||||||
const argument = tokens[1] ?? '';
|
const argument = tokens[1] ?? '';
|
||||||
return (
|
if (
|
||||||
command === 'su' &&
|
command === 'su' &&
|
||||||
(/^(?:root|-|--command|-c)$/.test(argument) || argument.startsWith('--command='))
|
(/^(?:root|-|--command|-c)$/.test(argument) || argument.startsWith('--command='))
|
||||||
);
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (command !== 'su') return false;
|
||||||
|
|
||||||
|
let index = 1;
|
||||||
|
while (index < tokens.length) {
|
||||||
|
const token = tokens[index] ?? '';
|
||||||
|
if (token === '-l' || token === '--login') {
|
||||||
|
return (tokens[index + 1] ?? '') === 'root';
|
||||||
|
}
|
||||||
|
if (token === '--') return (tokens[index + 1] ?? '') === 'root';
|
||||||
|
if (token.startsWith('-')) {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return token === 'root';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,8 +353,15 @@ const UNSAFE_EXAMPLE_FIXTURES = [
|
|||||||
source: 'env --unset FOO /usr/bin/apt-get install example',
|
source: 'env --unset FOO /usr/bin/apt-get install example',
|
||||||
},
|
},
|
||||||
{ expected: 'privileged-command', source: 'env -S apt-get install example' },
|
{ expected: 'privileged-command', source: 'env -S apt-get install example' },
|
||||||
|
{ expected: 'privileged-command', source: "env -S 'apt-get update'" },
|
||||||
|
{
|
||||||
|
expected: 'privileged-command',
|
||||||
|
source: "env --split-string='apt-get update'",
|
||||||
|
},
|
||||||
{ expected: 'privileged-command', source: 'reboot' },
|
{ expected: 'privileged-command', source: 'reboot' },
|
||||||
{ expected: 'privileged-command', source: 'su --command=/bin/sh' },
|
{ expected: 'privileged-command', source: 'su --command=/bin/sh' },
|
||||||
|
{ expected: 'privileged-command', source: 'su -l root' },
|
||||||
|
{ expected: 'privileged-command', source: '/usr/bin/su --login root' },
|
||||||
{ expected: 'privileged-command', source: 'su root' },
|
{ expected: 'privileged-command', source: 'su root' },
|
||||||
{ expected: 'privileged-command', source: '/usr/sbin/chroot /srv/example' },
|
{ expected: 'privileged-command', source: '/usr/sbin/chroot /srv/example' },
|
||||||
{ expected: 'credential-format', source: ['ghp_', 'a'.repeat(36)].join('') },
|
{ expected: 'credential-format', source: ['ghp_', 'a'.repeat(36)].join('') },
|
||||||
@@ -351,6 +413,10 @@ describe('documentation validation regressions', (): void => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows command availability queries without classifying the queried command', (): void => {
|
||||||
|
expect(exampleSafetyViolationKinds('command -v apt-get')).not.toContain('privileged-command');
|
||||||
|
});
|
||||||
|
|
||||||
it.each(UNSAFE_EXAMPLE_FIXTURES)(
|
it.each(UNSAFE_EXAMPLE_FIXTURES)(
|
||||||
'classifies unsafe example fixture as $expected',
|
'classifies unsafe example fixture as $expected',
|
||||||
({ expected, source }): void => {
|
({ expected, source }): void => {
|
||||||
|
|||||||
Reference in New Issue
Block a user