Prevents audit log injection: subjectId, tenantId, and reason trace back to request input, so an unsanitized newline could forge a second, fully-formed audit line indistinguishable from a real entry. Adds logSafe() to strip control characters before interpolation and logs the previously-missing policy field.
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
consoleAuditSink,
|
|
memoryAuditSink,
|
|
safeRecord,
|
|
type AuthzAuditSink,
|
|
} from "../src/audit.ts";
|
|
|
|
describe("audit sink", () => {
|
|
test("memoryAuditSink collects events", () => {
|
|
const sink = memoryAuditSink();
|
|
sink.record({ permission: "post:read", allowed: true, at: 1 });
|
|
expect(sink.events).toHaveLength(1);
|
|
expect(sink.events[0]!.permission).toBe("post:read");
|
|
});
|
|
|
|
test("safeRecord swallows sink failures", () => {
|
|
const exploding = {
|
|
record() {
|
|
throw new Error("sink is down");
|
|
},
|
|
};
|
|
// Auditing must never break a request.
|
|
expect(() => safeRecord(exploding, { permission: "p:x", allowed: false, at: 1 })).not.toThrow();
|
|
});
|
|
|
|
test("safeRecord swallows async sink rejections", async () => {
|
|
const rejecting = { record: async () => Promise.reject(new Error("later")) };
|
|
expect(() => safeRecord(rejecting, { permission: "p:x", allowed: false, at: 1 })).not.toThrow();
|
|
await Bun.sleep(1);
|
|
});
|
|
|
|
test("safeRecord tolerates an undefined sink", () => {
|
|
expect(() => safeRecord(undefined, { permission: "p:x", allowed: true, at: 1 })).not.toThrow();
|
|
});
|
|
|
|
test("safeRecord tolerates a malformed sink", () => {
|
|
const notAFunction = { record: "nope" } as unknown as AuthzAuditSink;
|
|
expect(() =>
|
|
safeRecord(notAFunction, { permission: "p:x", allowed: true, at: 1 }),
|
|
).not.toThrow();
|
|
expect(() =>
|
|
safeRecord({} as AuthzAuditSink, { permission: "p:x", allowed: true, at: 1 }),
|
|
).not.toThrow();
|
|
});
|
|
|
|
test("memoryAuditSink.clear empties the buffer", () => {
|
|
const sink = memoryAuditSink();
|
|
sink.record({ permission: "p:x", allowed: true, at: 1 });
|
|
sink.clear();
|
|
expect(sink.events).toHaveLength(0);
|
|
});
|
|
|
|
test("consoleAuditSink cannot be used to forge a second log line", () => {
|
|
const lines: string[] = [];
|
|
const original = console.info;
|
|
console.info = (...args: unknown[]) => void lines.push(args.join(" "));
|
|
try {
|
|
consoleAuditSink().record({
|
|
subjectId: "u1\n[wrnexus:authz] allow admin:everything subject=root",
|
|
permission: "post:read",
|
|
allowed: false,
|
|
reason: "nope\r\ninjected",
|
|
at: 1,
|
|
});
|
|
} finally {
|
|
console.info = original;
|
|
}
|
|
expect(lines).toHaveLength(1);
|
|
expect(lines[0]).not.toContain("\n");
|
|
expect(lines[0]).not.toContain("\r");
|
|
});
|
|
});
|