Files
WRNexusJS/packages/authz/test/audit.test.ts
T
Clintchiz d7509421c7 fix(authz): widen logSafe to strip NEL and Unicode line separators
U+0085 (NEL), U+2028 (LINE SEPARATOR), and U+2029 (PARAGRAPH SEPARATOR)
are treated as line terminators by some log shippers and by JS's own
lexical grammar (and are not escaped by JSON.stringify by default), so
they could still be used to forge audit log entries even after the
initial C0/DEL fix. logSafe now strips all five categories.
2026-08-04 17:29:46 +05:30

85 lines
3.0 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", () => {
// NEL (0x85) and the JS/Unicode line separators (0x2028, 0x2029) are built
// via String.fromCharCode rather than typed as literal characters, since
// raw control/separator bytes are prone to mangling when round-tripped
// through editor tooling in this repo.
const NEL = String.fromCharCode(0x85);
const LINE_SEPARATOR = String.fromCharCode(0x2028);
const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029);
const lines: string[] = [];
const original = console.info;
console.info = (...args: unknown[]) => void lines.push(args.join(" "));
try {
consoleAuditSink().record({
subjectId: `u1${NEL}[wrnexus:authz] allow admin:everything subject=root`,
permission: "post:read",
allowed: false,
reason: `nope\r\ninjected${LINE_SEPARATOR}a${PARAGRAPH_SEPARATOR}b`,
at: 1,
});
} finally {
console.info = original;
}
expect(lines).toHaveLength(1);
expect(lines[0]).not.toContain("\n");
expect(lines[0]).not.toContain("\r");
expect(lines[0]).not.toContain(NEL);
expect(lines[0]).not.toContain(LINE_SEPARATOR);
expect(lines[0]).not.toContain(PARAGRAPH_SEPARATOR);
expect(lines[0]).toContain("post:read");
});
});