Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/src/lib/audit-security.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ function stableStringify(value, depth = 0, seen = new WeakSet()) {
if (value === null || value === undefined) return "null";
if (typeof value !== "object") return JSON.stringify(value);

// Date has no own enumerable properties, so the generic Object.entries()
// path below silently serializes any Date to "{}", discarding the actual
// timestamp. old_value/new_value in a profile-change audit event can be a
// Date (e.g. a timestamp field changing), so this must be special-cased
// before the object/array branches — otherwise the audit trail records a
// meaningless "{}" instead of the value that actually changed.
if (value instanceof Date) {
return JSON.stringify(value.toISOString());
}

if (seen.has(value)) {
return '"[Circular]"';
}
Expand Down
15 changes: 15 additions & 0 deletions backend/src/lib/audit-security.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ describe("audit-security", () => {
expect(value).toBe('{"a":1,"b":2}');
});

it("preserves the timestamp when sanitizing a Date value (#1331)", () => {
// Date has no own enumerable properties, so the generic object-serialization
// path used to silently collapse any Date into "{}", losing the actual
// value entirely — e.g. a profile-change audit event recording a
// timestamp field change.
const date = new Date("2026-01-01T12:00:00.000Z");
expect(sanitizeAuditValue(date)).toBe('"2026-01-01T12:00:00.000Z"');
});

it("produces different hashes for payloads that differ only by Date value (#1331)", () => {
const before = hashAuditPayload({ old_value: new Date("2026-01-01T00:00:00.000Z") });
const after = hashAuditPayload({ old_value: new Date("2026-06-01T00:00:00.000Z") });
expect(before).not.toBe(after);
});

it("redacts sensitive audit field names", () => {
expect(sanitizeAuditKey("api_key")).toBe("[REDACTED]");
expect(sanitizeAuditKey("notification_email")).toBe("notification_email");
Expand Down