diff --git a/packages/authz/src/audit.ts b/packages/authz/src/audit.ts index ecfb0b34..177549b9 100644 --- a/packages/authz/src/audit.ts +++ b/packages/authz/src/audit.ts @@ -38,7 +38,11 @@ function logSafe(value: string): string { let out = ""; for (const character of value) { const code = character.codePointAt(0)!; - out += code < 0x20 || code === 0x7f ? " " : character; + // C0 + DEL, plus NEL and the Unicode line/paragraph separators, which some + // log shippers and JSON consumers also treat as line terminators. + const isLineBreaking = + code < 0x20 || code === 0x7f || code === 0x85 || code === 0x2028 || code === 0x2029; + out += isLineBreaking ? " " : character; } return out; } diff --git a/packages/authz/test/audit.test.ts b/packages/authz/test/audit.test.ts index 833c9651..3afe6844 100644 --- a/packages/authz/test/audit.test.ts +++ b/packages/authz/test/audit.test.ts @@ -52,15 +52,22 @@ describe("audit sink", () => { }); 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\n[wrnexus:authz] allow admin:everything subject=root", + subjectId: `u1${NEL}[wrnexus:authz] allow admin:everything subject=root`, permission: "post:read", allowed: false, - reason: "nope\r\ninjected", + reason: `nope\r\ninjected${LINE_SEPARATOR}a${PARAGRAPH_SEPARATOR}b`, at: 1, }); } finally { @@ -69,5 +76,9 @@ describe("audit sink", () => { 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"); }); });