Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 196 additions & 1 deletion packages/server-build/__tests__/security.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { assertAllowedCommand, assertNoFlagInjection } from "@paretools/shared";
import { z } from "zod";
import { assertAllowedCommand, assertNoFlagInjection, INPUT_LIMITS } from "@paretools/shared";

// ---------------------------------------------------------------------------
// assertAllowedCommand — used by the build tool
Expand Down Expand Up @@ -86,3 +87,197 @@ describe("assertNoFlagInjection", () => {
expect(() => assertNoFlagInjection("pages/about-us.tsx", "entryPoints")).not.toThrow();
});
});

// ---------------------------------------------------------------------------
// tsc tool — project parameter flag injection
// ---------------------------------------------------------------------------

describe("security: tsc tool — project parameter validation", () => {
it("accepts normal tsconfig paths", () => {
expect(() => assertNoFlagInjection("tsconfig.json", "project")).not.toThrow();
expect(() => assertNoFlagInjection("./tsconfig.build.json", "project")).not.toThrow();
expect(() => assertNoFlagInjection("packages/core/tsconfig.json", "project")).not.toThrow();
});

it("rejects flag-like project values", () => {
expect(() => assertNoFlagInjection("--outDir=/tmp/evil", "project")).toThrow(
/must not start with "-"/,
);
expect(() => assertNoFlagInjection("-p", "project")).toThrow(/must not start with "-"/);
expect(() => assertNoFlagInjection("--noEmit", "project")).toThrow(/must not start with "-"/);
expect(() => assertNoFlagInjection("--declaration", "project")).toThrow(
/must not start with "-"/,
);
});

it("rejects whitespace-prefixed flag injection", () => {
expect(() => assertNoFlagInjection(" --outDir=/etc/passwd", "project")).toThrow(
/must not start with "-"/,
);
expect(() => assertNoFlagInjection("\t-p", "project")).toThrow(/must not start with "-"/);
});
});

// ---------------------------------------------------------------------------
// vite-build tool — mode and args[] flag injection
// ---------------------------------------------------------------------------

describe("security: vite-build tool — mode parameter validation", () => {
it("accepts normal mode values", () => {
expect(() => assertNoFlagInjection("production", "mode")).not.toThrow();
expect(() => assertNoFlagInjection("development", "mode")).not.toThrow();
expect(() => assertNoFlagInjection("staging", "mode")).not.toThrow();
});

it("rejects flag-like mode values", () => {
expect(() => assertNoFlagInjection("--mode=evil", "mode")).toThrow(/must not start with "-"/);
expect(() => assertNoFlagInjection("-m", "mode")).toThrow(/must not start with "-"/);
expect(() => assertNoFlagInjection("--outDir=/etc/passwd", "mode")).toThrow(
/must not start with "-"/,
);
});
});

describe("security: vite-build tool — args[] parameter validation", () => {
it("rejects flag-like args values", () => {
expect(() => assertNoFlagInjection("--outDir=/tmp/evil", "args")).toThrow(
/must not start with "-"/,
);
expect(() => assertNoFlagInjection("-w", "args")).toThrow(/must not start with "-"/);
expect(() => assertNoFlagInjection("--base=/evil", "args")).toThrow(/must not start with "-"/);
});
});

// ---------------------------------------------------------------------------
// webpack tool — config and args[] flag injection
// ---------------------------------------------------------------------------

describe("security: webpack tool — config parameter validation", () => {
it("accepts normal config paths", () => {
expect(() => assertNoFlagInjection("webpack.config.js", "config")).not.toThrow();
expect(() => assertNoFlagInjection("./config/webpack.prod.js", "config")).not.toThrow();
});

it("rejects flag-like config values", () => {
expect(() => assertNoFlagInjection("--output-path=/etc/passwd", "config")).toThrow(
/must not start with "-"/,
);
expect(() => assertNoFlagInjection("-c", "config")).toThrow(/must not start with "-"/);
expect(() => assertNoFlagInjection("--mode=production", "config")).toThrow(
/must not start with "-"/,
);
});

it("rejects whitespace-prefixed flag injection", () => {
expect(() => assertNoFlagInjection(" --output-path=/tmp", "config")).toThrow(
/must not start with "-"/,
);
});
});

describe("security: webpack tool — args[] parameter validation", () => {
it("rejects flag-like args values", () => {
expect(() => assertNoFlagInjection("--output-path=/tmp/evil", "args")).toThrow(
/must not start with "-"/,
);
expect(() => assertNoFlagInjection("--mode=production", "args")).toThrow(
/must not start with "-"/,
);
expect(() => assertNoFlagInjection("-d", "args")).toThrow(/must not start with "-"/);
});
});

// ---------------------------------------------------------------------------
// build tool — args[] validation (note: command uses assertAllowedCommand)
// ---------------------------------------------------------------------------

describe("security: build tool — args[] parameter validation", () => {
it("rejects flag-like args values", () => {
expect(() => assertNoFlagInjection("--exec=rm -rf /", "args")).toThrow(
/must not start with "-"/,
);
expect(() => assertNoFlagInjection("-rf", "args")).toThrow(/must not start with "-"/);
expect(() => assertNoFlagInjection("--output=/etc/passwd", "args")).toThrow(
/must not start with "-"/,
);
});

it("accepts safe args values", () => {
expect(() => assertNoFlagInjection("run", "args")).not.toThrow();
expect(() => assertNoFlagInjection("build", "args")).not.toThrow();
expect(() => assertNoFlagInjection("src/index.ts", "args")).not.toThrow();
});
});

// ---------------------------------------------------------------------------
// Zod .max() input-limit constraints — Build tool schemas
// ---------------------------------------------------------------------------

describe("Zod .max() constraints — Build tool schemas", () => {
describe("tsc project parameter (PATH_MAX)", () => {
const schema = z.string().max(INPUT_LIMITS.PATH_MAX);

it("accepts a path within the limit", () => {
expect(schema.safeParse("tsconfig.json").success).toBe(true);
});

it("rejects a path exceeding PATH_MAX", () => {
const oversized = "p".repeat(INPUT_LIMITS.PATH_MAX + 1);
expect(schema.safeParse(oversized).success).toBe(false);
});
});

describe("vite-build mode parameter (SHORT_STRING_MAX)", () => {
const schema = z.string().max(INPUT_LIMITS.SHORT_STRING_MAX);

it("accepts a mode within the limit", () => {
expect(schema.safeParse("production").success).toBe(true);
});

it("rejects a mode exceeding SHORT_STRING_MAX", () => {
const oversized = "m".repeat(INPUT_LIMITS.SHORT_STRING_MAX + 1);
expect(schema.safeParse(oversized).success).toBe(false);
});
});

describe("build command parameter (SHORT_STRING_MAX)", () => {
const schema = z.string().max(INPUT_LIMITS.SHORT_STRING_MAX);

it("accepts a command within the limit", () => {
expect(schema.safeParse("npm").success).toBe(true);
});

it("rejects a command exceeding SHORT_STRING_MAX", () => {
const oversized = "c".repeat(INPUT_LIMITS.SHORT_STRING_MAX + 1);
expect(schema.safeParse(oversized).success).toBe(false);
});
});

describe("args array (ARRAY_MAX + STRING_MAX)", () => {
const schema = z.array(z.string().max(INPUT_LIMITS.STRING_MAX)).max(INPUT_LIMITS.ARRAY_MAX);

it("rejects array exceeding ARRAY_MAX", () => {
const oversized = Array.from({ length: INPUT_LIMITS.ARRAY_MAX + 1 }, () => "arg");
expect(schema.safeParse(oversized).success).toBe(false);
});

it("rejects arg string exceeding STRING_MAX", () => {
const oversized = ["a".repeat(INPUT_LIMITS.STRING_MAX + 1)];
expect(schema.safeParse(oversized).success).toBe(false);
});
});

describe("esbuild entryPoints (ARRAY_MAX + PATH_MAX)", () => {
const schema = z.array(z.string().max(INPUT_LIMITS.PATH_MAX)).max(INPUT_LIMITS.ARRAY_MAX);

it("rejects array exceeding ARRAY_MAX", () => {
const oversized = Array.from({ length: INPUT_LIMITS.ARRAY_MAX + 1 }, () => "src/index.ts");
expect(schema.safeParse(oversized).success).toBe(false);
});

it("rejects path exceeding PATH_MAX", () => {
const oversized = ["p".repeat(INPUT_LIMITS.PATH_MAX + 1)];
expect(schema.safeParse(oversized).success).toBe(false);
});
});
});
110 changes: 109 additions & 1 deletion packages/server-cargo/__tests__/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
* "--manifest-path=/evil" could be interpreted as a flag.
*/
import { describe, it, expect } from "vitest";
import { assertNoFlagInjection } from "@paretools/shared";
import { z } from "zod";
import { assertNoFlagInjection, INPUT_LIMITS } from "@paretools/shared";

/** Malicious inputs that must be rejected by every guarded parameter. */
const MALICIOUS_INPUTS = [
Expand Down Expand Up @@ -95,3 +96,110 @@ describe("security: cargo run — args validation", () => {
}
});
});

// ---------------------------------------------------------------------------
// cargo build — no user-supplied string params (only path and boolean)
// ---------------------------------------------------------------------------

describe("security: cargo build — no injectable string params", () => {
it("build tool only accepts path (PATH_MAX) and release (boolean)", () => {
// The build tool has no user-facing string params that could be flag-injected.
// path is validated by Zod .max(PATH_MAX) and release is a boolean.
// This test documents that explicitly.
const pathSchema = z.string().max(INPUT_LIMITS.PATH_MAX).optional();
const releaseSchema = z.boolean().optional();

expect(pathSchema.safeParse("/some/path").success).toBe(true);
expect(releaseSchema.safeParse(true).success).toBe(true);
expect(releaseSchema.safeParse("--release").success).toBe(false);
});
});

// ---------------------------------------------------------------------------
// cargo clippy — no user-supplied string params (only path)
// ---------------------------------------------------------------------------

describe("security: cargo clippy — no injectable string params", () => {
it("clippy tool only accepts path (PATH_MAX)", () => {
// The clippy tool has no user-facing string params that could be flag-injected.
// path is validated by Zod .max(PATH_MAX).
const pathSchema = z.string().max(INPUT_LIMITS.PATH_MAX).optional();

expect(pathSchema.safeParse("/some/path").success).toBe(true);
expect(pathSchema.safeParse("p".repeat(INPUT_LIMITS.PATH_MAX + 1)).success).toBe(false);
});
});

// ---------------------------------------------------------------------------
// cargo fmt — no user-supplied string params (only path and boolean)
// ---------------------------------------------------------------------------

describe("security: cargo fmt — no injectable string params", () => {
it("fmt tool only accepts path (PATH_MAX) and check (boolean)", () => {
// The fmt tool has no user-facing string params that could be flag-injected.
// path is validated by Zod .max(PATH_MAX) and check is a boolean.
const pathSchema = z.string().max(INPUT_LIMITS.PATH_MAX).optional();
const checkSchema = z.boolean().optional();

expect(pathSchema.safeParse("./my-project").success).toBe(true);
expect(checkSchema.safeParse(true).success).toBe(true);
expect(checkSchema.safeParse("--check").success).toBe(false);
});
});

// ---------------------------------------------------------------------------
// cargo remove — packages[] validation
// ---------------------------------------------------------------------------

describe("security: cargo remove — packages[] validation", () => {
it("rejects flag-like package names", () => {
for (const malicious of MALICIOUS_INPUTS) {
expect(() => assertNoFlagInjection(malicious, "packages")).toThrow(/must not start with "-"/);
}
});

it("accepts safe package names", () => {
for (const safe of SAFE_INPUTS) {
expect(() => assertNoFlagInjection(safe, "packages")).not.toThrow();
}
});
});

// ---------------------------------------------------------------------------
// Zod .max() input-limit constraints — Cargo tool schemas
// ---------------------------------------------------------------------------

describe("Zod .max() constraints — Cargo tool schemas", () => {
describe("path parameter (PATH_MAX)", () => {
const schema = z.string().max(INPUT_LIMITS.PATH_MAX);

it("accepts a path within the limit", () => {
expect(schema.safeParse("/home/user/project").success).toBe(true);
});

it("rejects a path exceeding PATH_MAX", () => {
const oversized = "p".repeat(INPUT_LIMITS.PATH_MAX + 1);
expect(schema.safeParse(oversized).success).toBe(false);
});
});

describe("packages array (ARRAY_MAX + SHORT_STRING_MAX)", () => {
const schema = z
.array(z.string().max(INPUT_LIMITS.SHORT_STRING_MAX))
.max(INPUT_LIMITS.ARRAY_MAX);

it("rejects array exceeding ARRAY_MAX", () => {
const oversized = Array.from({ length: INPUT_LIMITS.ARRAY_MAX + 1 }, (_, i) => `pkg-${i}`);
expect(schema.safeParse(oversized).success).toBe(false);
});

it("rejects package name exceeding SHORT_STRING_MAX", () => {
const oversized = ["x".repeat(INPUT_LIMITS.SHORT_STRING_MAX + 1)];
expect(schema.safeParse(oversized).success).toBe(false);
});

it("accepts normal package names", () => {
expect(schema.safeParse(["serde", "tokio", "reqwest"]).success).toBe(true);
});
});
});
Loading
Loading