T-HUB/packages/media-sandbox/src/runtime.test.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

103 lines
7.3 KiB
TypeScript

import { spawn, spawnSync } from "node:child_process";
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterAll, describe, expect, it } from "vitest";
import { MediaCommandBuilder } from "./command";
import { collectProbe } from "./probe";
import { sandboxEnvironment } from "./profile";
import { helperPath, sandboxCapability, spawnSandbox, type SandboxChild, type SandboxOptions } from "./runtime";
const work = mkdtempSync(join(tmpdir(), "media-security-tests-"));
const rootfs = join(work, "rootfs");
for (const dir of ["upload", "scratch", "tmp", "proc", "dev", "usr/bin"]) mkdirSync(join(rootfs, dir), { recursive: true });
writeFileSync(join(rootfs, "dev/null"), "");
const built = spawnSync("cc", ["-static", "-O0", "-Wall", "-Wextra", "-Werror", resolve(__dirname, "../native/test-child.c"), "-o", join(rootfs, "usr/bin/ffprobe")]);
if (built.status === 0) spawnSync("cp", [join(rootfs, "usr/bin/ffprobe"), join(rootfs, "usr/bin/ffmpeg")]);
const options = { rootfs, cgroupRoot: process.env.MEDIA_SANDBOX_CGROUP_ROOT ?? "/sys/fs/cgroup" };
const capability = built.status === 0 ? sandboxCapability(options) : { available: false, reason: "static C test fixture compiler unavailable" };
if (!capability.available) console.warn(`SKIP sandbox integration and sandbox canaries: ${capability.reason}`);
afterAll(() => rmSync(work, { recursive: true, force: true }));
const builder = new MediaCommandBuilder(); let sequence = 0;
function job(action: string, limits?: SandboxOptions["limits"]): SandboxChild {
const id = (++sequence).toString(16).padStart(64, "0"); const file = join(work, id);
writeFileSync(file, action);
return spawnSandbox(builder.probe(id, "wav"), { ...options, limits, asset: { id, resolve: () => file } }, { stdio: ["ignore", "pipe", "pipe"] });
}
async function output(child: SandboxChild): Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }> {
let stdout = "", stderr = "";
child.child.stdout?.on("data", chunk => { stdout += String(chunk); });
child.child.stderr?.on("data", chunk => { stderr += String(chunk); });
return { ...await child.exited, stdout, stderr };
}
function ready(child: SandboxChild): Promise<void> {
return new Promise((done, reject) => { child.child.stdout!.once("data", () => done()); child.child.once("exit", () => reject(new Error("sandbox failed before ready"))); });
}
describe.skipIf(!capability.available)("rootless sandbox confinement", () => {
it("cannot read host/vault/DB/other-job files; root is read-only and scratch works; UIDs differ", async () => {
const secret = join(work, "vault-db-sibling-secret"); writeFileSync(secret, "private");
const first = await output(job(`isolation\n${secret}\n`));
const second = await output(job(`isolation\n${secret}\n`));
expect(first.code, first.stderr).toBe(0); expect(second.code, second.stderr).toBe(0);
expect(first.stdout).toContain("isolated uid="); expect(first.stdout).not.toEqual(second.stdout);
});
it("blocks socket creation, DNS, loopback, internal CIDRs and metadata", async () => {
const result = await output(job("network\n")); expect(result.code, result.stderr).toBe(0); expect(result.stdout).toContain("network denied");
});
it("kills memory/time offenders without affecting a sibling", async () => {
const sibling = job("sleep\n", { wallTimeMs: 30_000 });
try {
await ready(sibling);
const memory = await output(job("memory\n", { memoryBytes: 16 * 1024 * 1024, wallTimeMs: 5000 }));
expect(memory.stdout).toContain("ready"); expect(memory.signal).toBe("SIGKILL");
expect(sibling.child.exitCode).toBeNull(); expect(sibling.child.signalCode).toBeNull();
const timed = await output(job("sleep\n", { wallTimeMs: 1000 })); expect(timed.stdout).toContain("ready"); expect(timed.signal).toBe("SIGKILL");
expect(sibling.child.exitCode).toBeNull(); expect(sibling.child.signalCode).toBeNull();
} finally { sibling.kill(); await sibling.exited; }
}, 15_000);
it("enforces pids and file-size caps without affecting siblings", async () => {
const sibling = job("sleep\n", { wallTimeMs: 30_000 });
try {
await ready(sibling);
const pids = await output(job("pids\n", { pids: 12 })); expect(pids.code, pids.stderr).toBe(0); expect(pids.stdout).toContain("pids denied");
const size = await output(job("size\n", { fileSizeBytes: 8192 })); expect(size.code).not.toBe(0);
expect(sibling.child.exitCode).toBeNull(); expect(sibling.child.signalCode).toBeNull();
} finally { sibling.kill(); await sibling.exited; }
}, 15_000);
it("caps and validates hostile probe output", async () => {
expect((await collectProbe(job("probe-good\n"))).streams).toHaveLength(1);
await expect(collectProbe(job("probe-big\n"), 4096)).rejects.toThrow(/limit exceeded/);
await expect(collectProbe(job("probe-invalid\n"))).rejects.toThrow(/invalid probe/);
});
it("canary key/token/passphrase stay absent from every process cmdline/environ and captured output/log", async () => {
const canaries = ["stream-key-canary-95c70", "oauth-token-canary-d7921", "srt-passphrase-canary-f1f12"];
const names = ["CANARY_STREAM_KEY", "CANARY_OAUTH_TOKEN", "CANARY_SRT_PASSPHRASE"];
const before = names.map(name => process.env[name]); names.forEach((name, i) => { process.env[name] = canaries[i]; });
try {
const child = job("metadata\n");
const captured = output(child);
// The host-visible wrapper stays alive while the child inspects its final /proc metadata itself.
await new Promise(done => setTimeout(done, 100));
const metadata = [readFileSync(`/proc/${child.child.pid}/cmdline`), readFileSync(`/proc/${child.child.pid}/environ`)];
const result = await captured;
expect(result.code).toBe(7);
expect(result.stdout).toContain("PATH=/usr/bin:/bin");
expect(result.stderr).toContain("forced authentication/connection failure");
for (const canary of canaries) for (const text of [...metadata.map(v => v.toString()), result.stdout, result.stderr]) expect(text).not.toContain(canary);
} finally { names.forEach((name, i) => { if (before[i] === undefined) delete process.env[name]; else process.env[name] = before[i]; }); }
});
});
it("probe collector caps output and never logs hostile stderr, even without sandbox capability", async () => {
const canary = "stream-key-canary-oversized"; const logs: string[] = [];
const child = spawn(process.execPath, ["-e", 'process.stdin.once("data",b=>{process.stderr.write(b);process.stdout.write("x".repeat(50000))})'], { env: sandboxEnvironment(), stdio: ["pipe", "pipe", "pipe"] });
const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(done => child.once("close", (code, signal) => done({ code, signal })));
const collected = collectProbe({ child, exited, kill: () => { child.kill("SIGKILL"); } }, 4096, line => logs.push(line));
child.stdin!.end(canary);
await expect(collected).rejects.toThrow(/limit exceeded/);
expect(logs.length).toBeGreaterThan(0); expect(logs.join("\n")).not.toContain(canary);
});
it("installs the real seccomp profile and no-new-privileges without needing userns", () => {
const result = spawnSync(helperPath(), ["--seccomp-check"], { env: sandboxEnvironment(), timeout: 5000 });
expect(result.status, String(result.stderr)).toBe(0);
});