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.
This commit is contained in:
2026-08-04 17:29:46 +05:30
parent d609a41222
commit d7509421c7
2 changed files with 18 additions and 3 deletions
+5 -1
View File
@@ -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;
}
+13 -2
View File
@@ -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");
});
});