Files
SkyMoney/api/tests/injection-safety.test.ts
Ricearoni1245 1645896e54
Some checks failed
Deploy / deploy (push) Has been cancelled
chore: ran security check for OWASP top 10
2026-03-01 20:44:55 -06:00

100 lines
3.7 KiB
TypeScript

import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
describe("A05 Injection Safety", () => {
function runNormalizedScript(scriptPath: string, env: Record<string, string>) {
const tmpScriptDir = mkdtempSync(join(tmpdir(), "skymoney-a05-script-"));
const normalizedScript = join(tmpScriptDir, "script.sh");
writeFileSync(normalizedScript, readFileSync(scriptPath, "utf8").replace(/\r/g, ""));
chmodSync(normalizedScript, 0o755);
const res = spawnSync("bash", [normalizedScript], {
env: { ...process.env, ...env },
encoding: "utf8",
});
rmSync(tmpScriptDir, { recursive: true, force: true });
return res;
}
it("contains no unsafe Prisma raw SQL APIs across API source", () => {
const apiRoot = resolve(__dirname, "..");
const badPatterns = ["$queryRawUnsafe(", "$executeRawUnsafe("];
const queue = [resolve(apiRoot, "src")];
const offenders: string[] = [];
while (queue.length > 0) {
const current = queue.pop() as string;
for (const name of readdirSync(current)) {
const full = join(current, name);
if (statSync(full).isDirectory()) {
queue.push(full);
continue;
}
if (!full.endsWith(".ts")) continue;
const content = readFileSync(full, "utf8");
if (badPatterns.some((p) => content.includes(p))) offenders.push(full);
}
}
expect(offenders).toEqual([]);
});
it("rejects malicious restore DB identifiers before DB command execution", () => {
const repoRoot = resolve(__dirname, "..", "..");
const restoreScriptPath = resolve(repoRoot, "scripts/restore.sh");
const tmpRoot = mkdtempSync(join(tmpdir(), "skymoney-a05-restore-"));
const fakeBin = join(tmpRoot, "bin");
const backupFile = join(tmpRoot, "sample.dump");
const checksumFile = `${backupFile}.sha256`;
const markerFile = join(tmpRoot, "db_called.marker");
try {
mkdirSync(fakeBin, { recursive: true });
writeFileSync(backupFile, "valid-content");
const backupBytes = readFileSync(backupFile);
const hash = createHash("sha256").update(backupBytes).digest("hex");
writeFileSync(checksumFile, `${hash} sample.dump\n`);
const fakePsql = join(fakeBin, "psql");
const fakePgRestore = join(fakeBin, "pg_restore");
const fakeSha256sum = join(fakeBin, "sha256sum");
writeFileSync(fakePsql, `#!/usr/bin/env bash\nset -euo pipefail\ntouch "${markerFile}"\n`);
writeFileSync(fakePgRestore, `#!/usr/bin/env bash\nset -euo pipefail\ntouch "${markerFile}"\n`);
writeFileSync(
fakeSha256sum,
`#!/usr/bin/env bash\nset -euo pipefail\necho "${hash} $1"\n`
);
chmodSync(fakePsql, 0o755);
chmodSync(fakePgRestore, 0o755);
chmodSync(fakeSha256sum, 0o755);
const res = runNormalizedScript(restoreScriptPath, {
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
BACKUP_FILE: backupFile,
DATABASE_URL: "postgres://app:app@127.0.0.1:5432/skymoney",
RESTORE_DATABASE_URL: "postgres://app:app@127.0.0.1:5432/skymoney_restore_test",
RESTORE_DB: 'bad-name"; DROP DATABASE skymoney; --',
});
expect(res.status).not.toBe(0);
expect(`${res.stdout}${res.stderr}`).toContain("RESTORE_DB must match");
expect(existsSync(markerFile)).toBe(false);
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
});
});