diff --git a/src/guardrails/index.ts b/src/guardrails/index.ts new file mode 100644 index 0000000..89b6575 --- /dev/null +++ b/src/guardrails/index.ts @@ -0,0 +1,44 @@ +// List of dangerous command patterns that should be blocked +// Patterns are case-insensitive and stored in lowercase for efficiency +// +// NOTE: This is a basic substring-based approach that provides a first line of defense. +// Future improvements could include: +// - More comprehensive pattern list (sudo variants, different spacing, etc.) +// - Word boundary checking to avoid false positives on filenames +// - Command structure parsing to identify actual operations +// - Allowlist approach for additional security +const DENYLIST = [ + "rm -rf /", + "rm -rf/*", + "rm -rf /.", + "shutdown", + "reboot", + ":(){ :|:& };:", + "mkfs", + "dd if=", +]; + +// Pre-computed lowercase versions for efficient checking +const DENYLIST_LOWER = DENYLIST.map((token) => token.toLowerCase()); + +/** + * Enforces guardrails to prevent execution of dangerous shell commands + * @param command The command to check + * @param allowShell Whether shell commands are allowed + * @throws Error if the command is blocked by guardrails + */ +export function enforceGuardrails(command: string, allowShell: boolean): void { + if (!allowShell) { + throw new Error("Shell commands are disabled by configuration."); + } + + const lower = command.toLowerCase(); + const blockedIndex = DENYLIST_LOWER.findIndex((token) => + lower.includes(token), + ); + if (blockedIndex !== -1) { + throw new Error( + `Command blocked by guardrails: ${DENYLIST[blockedIndex]}`, + ); + } +} diff --git a/src/loz.ts b/src/loz.ts index ed03ee0..05afa8d 100644 --- a/src/loz.ts +++ b/src/loz.ts @@ -15,6 +15,7 @@ import { requestApiKey, } from "./config"; import { Git } from "./git"; +import { enforceGuardrails } from "./guardrails"; // Get the path to the home directory const HOME_PATH = os.homedir() || ""; @@ -563,6 +564,8 @@ export class Loz { if (answer.toLowerCase() === "y") { for (const cmd of commands) { try { + // Enforce guardrails to prevent dangerous commands + enforceGuardrails(cmd, true); await runCommand(cmd); } catch (error: any) { if (typeof error === "string" && error.indexOf("No output") === 0) { diff --git a/test/guardrails.test.ts b/test/guardrails.test.ts new file mode 100644 index 0000000..251f1c9 --- /dev/null +++ b/test/guardrails.test.ts @@ -0,0 +1,84 @@ +import { expect } from "chai"; +import "mocha"; +import { enforceGuardrails } from "../src/guardrails"; + +describe("Guardrails Test", () => { + describe("enforceGuardrails", () => { + it("should throw error when shell is disabled", () => { + expect(() => enforceGuardrails("ls -la", false)).to.throw( + "Shell commands are disabled by configuration.", + ); + }); + + it("should allow safe commands", () => { + expect(() => enforceGuardrails("ls -la", true)).to.not.throw(); + expect(() => enforceGuardrails("cd /tmp", true)).to.not.throw(); + expect(() => enforceGuardrails("echo hello", true)).to.not.throw(); + expect(() => enforceGuardrails("cat file.txt", true)).to.not.throw(); + }); + + it("should block rm -rf /", () => { + expect(() => enforceGuardrails("rm -rf /", true)).to.throw( + "Command blocked by guardrails: rm -rf /", + ); + expect(() => enforceGuardrails("RM -RF /", true)).to.throw( + "Command blocked by guardrails: rm -rf /", + ); + }); + + it("should block rm -rf / bypass attempts", () => { + expect(() => enforceGuardrails("rm -rf/*", true)).to.throw( + "Command blocked by guardrails", + ); + expect(() => enforceGuardrails("rm -rf /.", true)).to.throw( + "Command blocked by guardrails", + ); + }); + + it("should block shutdown", () => { + expect(() => enforceGuardrails("shutdown now", true)).to.throw( + "Command blocked by guardrails: shutdown", + ); + expect(() => enforceGuardrails("SHUTDOWN", true)).to.throw( + "Command blocked by guardrails: shutdown", + ); + }); + + it("should block reboot", () => { + expect(() => enforceGuardrails("reboot", true)).to.throw( + "Command blocked by guardrails: reboot", + ); + expect(() => enforceGuardrails("sudo reboot", true)).to.throw( + "Command blocked by guardrails: reboot", + ); + }); + + it("should block fork bomb", () => { + expect(() => enforceGuardrails(":(){ :|:& };:", true)).to.throw( + "Command blocked by guardrails: :(){ :|:& };:", + ); + }); + + it("should block mkfs", () => { + expect(() => enforceGuardrails("mkfs /dev/sda", true)).to.throw( + "Command blocked by guardrails: mkfs", + ); + expect(() => enforceGuardrails("mkfs.ext4 /dev/sda1", true)).to.throw( + "Command blocked by guardrails: mkfs", + ); + }); + + it("should block dd if=", () => { + expect(() => + enforceGuardrails("dd if=/dev/zero of=/dev/sda", true), + ).to.throw("Command blocked by guardrails: dd if="); + }); + + it("should allow commands with similar but safe patterns", () => { + // These should be allowed as they don't match the blocked patterns + expect(() => enforceGuardrails("rm file.txt", true)).to.not.throw(); + expect(() => enforceGuardrails("rm -r /tmp/test", true)).to.not.throw(); + expect(() => enforceGuardrails("dd status=progress", true)).to.not.throw(); + }); + }); +});