Let's talk
security

Our audit log said nothing changed when a password did

An audit trail hid the request but stored old and new password values in clear, then showed a password change as no change. Both faults and the fixes.

· · updated

A laptop on a workshop bench showing the audit log in Sazinga Comply.

Somebody asks you the question every audit and every security review asks: who changed this, and what did it change to? Your system keeps a trail for exactly that. In ours, the trail got two things wrong about the most sensitive fields, and the second mistake appeared while we were fixing the first.

First, for any change to a sensitive field such as a password, the trail held the old value and the new value in plain text, in a table that anyone with audit permission could read. Second, once we hid those values, the screen showed a password change as “unchanged”. A trail that leaks a secret is a risk. A trail that says nothing happened on the one event a reviewer is looking at is worse, because it reads with the full authority of a system of record.

What was actually going on

For every change the trail stores two copies of the facts: the request that caused it, and a field-by-field list of what actually changed, old value beside new value. Sensitive fields in the request had been masked before storage. That had been reviewed, tested and signed off.

The field-by-field list had not been masked. So next to a carefully scrubbed request sat a plain record of exactly the values that had been scrubbed. The masking was real. It had been applied to one of the two places the data was written, and the review had looked at the masking code, which was correct, rather than at where the data went.

The second fault was on the screen. After the fix, a masked field showed the hiding marker on both sides, old and new. The viewer compared the two, found them identical and displayed the field as unchanged. So the detail panel for a password change told the reader that nothing about the password had changed.

There was a third, quieter gap. An entry that says a record changed, with a time, is nearly useless in an investigation. You need who did it, from where and with what authority. All of that was being recorded: the user, whether they acted as staff, as a platform administrator or as an outside party, the address and client they came from, and whether they were impersonating somebody. None of it was being shown.

What we changed

We hid the sensitive values in the field-by-field list as well, both when an entry is written and when it is read. Writing covers everything from now on. Reading covers the rows that were stored before the fix, which would otherwise leak for ever.

The screen now tells three cases apart instead of two. Both sides withheld reads as “changed, value withheld”: it happened, you may not see what it was. One side withheld gets a marker on that side, so a partial view looks partial. The panel also says how many fields on the event were withheld, so the reader knows they are not seeing everything. The request itself became a per-field list with the same treatment.

Two kinds of event were never going to be caught by the automatic trail, because it only notices rows being written. Exporting a report changes nothing, yet a user taking a copy of the customer list is precisely what a review wants to see, so that is recorded by an explicit call. And a role’s permissions can be rewritten quickly by a bulk operation that the trail cannot see, so we did it the slower way that it can, with an explicit entry naming the role and the change. The reason is written next to the code so nobody optimises it away.

What it did not fix

The screen still shows an identifier rather than a person’s name in places. Showing names needs a way to fetch a single member’s details, and that did not exist. We logged it as required work rather than download a hundred records to label one row, and this write-up does not claim it is built.

And none of this protects against someone with direct access to the database. The trail was never meant to. The person it guards against is an ordinary user of the product, and the failure it prevents is a record that quietly says the wrong thing.

The pattern, for anyone who relies on an audit log

Ask where each sensitive value is copied. A request and a before-and-after list are two copies, and so are a log line and a database column. Hiding one is not hiding the value.

Ask whether hiding happens when data is written and when it is read, because the past rows matter too. Ask what the screen says when a value is withheld: “changed, value withheld” is honest, “unchanged” is false. And ask whether every entry says who, from where and with what authority.

The practical test takes five minutes. Have somebody change a password in a test account, then open the audit entry for it. If you see the password, or you see “no change”, you have found the problem before an auditor does.

Where this ends up

That is the standard the audit log in Sazinga Comply has to meet, because who closed an obligation, when and with what evidence is the thing being relied on, not a by-product of saving a record.

This came out of building Sazinga Comply

Know what is due, what is evidenced and what has expired. The problem above is one we met while building it, and what we did about it is in the product.

If you run something like this, there is one thing you can do without a call: send one inspection checklist you use today.