fix(authz): sanitize control characters in console audit sink

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.
This commit is contained in:
2026-08-04 17:21:09 +05:30
parent ba83038d8d
commit e710756baf
2 changed files with 62 additions and 4 deletions
+43 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, test } from "bun:test";
import { memoryAuditSink, safeRecord } from "../src/audit.ts";
import {
consoleAuditSink,
memoryAuditSink,
safeRecord,
type AuthzAuditSink,
} from "../src/audit.ts";
describe("audit sink", () => {
test("memoryAuditSink collects events", () => {
@@ -28,4 +33,41 @@ describe("audit sink", () => {
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");
});
});