fix(security): harden safeBins long-option validation

This commit is contained in:
Peter Steinberger
2026-02-23 23:55:28 +00:00
parent 7b4d2cb5cb
commit 3b8e33037a
4 changed files with 136 additions and 8 deletions

View File

@@ -134,17 +134,23 @@ export const SAFE_BIN_PROFILE_FIXTURES: Record<string, SafeBinProfileFixture> =
"--key",
"--field-separator",
"--buffer-size",
"--temporary-directory",
"--parallel",
"--batch-size",
"--random-source",
"-k",
"-t",
"-S",
"-T",
],
// --compress-program can invoke an external executable and breaks stdin-only guarantees.
deniedFlags: ["--compress-program", "--files0-from", "--output", "-o"],
// --random-source/--temporary-directory/-T are filesystem-dependent and not stdin-only.
deniedFlags: [
"--compress-program",
"--files0-from",
"--output",
"--random-source",
"--temporary-directory",
"-T",
"-o",
],
},
uniq: {
maxPositional: 0,
@@ -293,6 +299,38 @@ function isInvalidValueToken(value: string | undefined): boolean {
return !value || !isSafeLiteralToken(value);
}
function collectKnownLongFlags(
allowedValueFlags: ReadonlySet<string>,
deniedFlags: ReadonlySet<string>,
): string[] {
const known = new Set<string>();
for (const flag of allowedValueFlags) {
if (flag.startsWith("--")) {
known.add(flag);
}
}
for (const flag of deniedFlags) {
if (flag.startsWith("--")) {
known.add(flag);
}
}
return Array.from(known);
}
function resolveCanonicalLongFlag(flag: string, knownLongFlags: string[]): string | null {
if (!flag.startsWith("--") || flag.length <= 2) {
return null;
}
if (knownLongFlags.includes(flag)) {
return flag;
}
const matches = knownLongFlags.filter((candidate) => candidate.startsWith(flag));
if (matches.length !== 1) {
return null;
}
return matches[0] ?? null;
}
function consumeLongOptionToken(
args: string[],
index: number,
@@ -301,13 +339,22 @@ function consumeLongOptionToken(
allowedValueFlags: ReadonlySet<string>,
deniedFlags: ReadonlySet<string>,
): number {
if (deniedFlags.has(flag)) {
const knownLongFlags = collectKnownLongFlags(allowedValueFlags, deniedFlags);
const canonicalFlag = resolveCanonicalLongFlag(flag, knownLongFlags);
if (!canonicalFlag) {
return -1;
}
if (deniedFlags.has(canonicalFlag)) {
return -1;
}
const expectsValue = allowedValueFlags.has(canonicalFlag);
if (inlineValue !== undefined) {
if (!expectsValue) {
return -1;
}
return isSafeLiteralToken(inlineValue) ? index + 1 : -1;
}
if (!allowedValueFlags.has(flag)) {
if (!expectsValue) {
return index + 1;
}
return isInvalidValueToken(args[index + 1]) ? -1 : index + 2;