September 15, 2026

the code was the clean part

By Geoffrey Broomfield ยท 3 min read

I asked for a security audit of the thing I’m building. I didn’t want to be the reason a customer got compromised.

It came back, and the code was fine.

That was the surprise. The careful parts held. Tokens get refreshed before they expire. Request signatures get compared in a way that doesn’t leak the answer one byte at a time. Credentials get masked before anything writes them to a log. I’d assumed the code was where the damage would be.

Everything that was wrong was around it.

The worst of it is a folder. A container mounts a config directory and points the token store at it. The first time somebody authorizes an account inside that container, live refresh tokens land in their working directory. The image is protected โ€” a rule keeps credentials out of it โ€” and the file that tells git what to ignore knows a folder called .tokens and nothing about this one. So the direction nobody checked was the one that mattered. A refresh token that reaches a git history is permanent. Rotating it is the only remedy.

Two of the findings were documents.

One of mine said to run a command that locks the credentials file down. A paragraph, addressed to a reader, in a repository where that file holds production keys and gets read unattended every fifteen minutes. A step in a document isn’t a step. The tool now narrows the credentials it’s about to read, on every run, and says so when it changes something.

The other was a comment above the code that masks credentials in logs. It said a credential stays masked even if it turns up somewhere unexpected, and it offered a traceback as the example. I ran it. The traceback printed the token in full. A traceback isn’t built from the log message. The formatter builds it afterwards, from a field my filter never touched. The comment described a guarantee the code didn’t make.

Then the packaging.

One installer registered a job to rebuild a page every fifteen minutes. It printed "every fifteen minutes" to whoever was watching. Then it handed the same number to the scheduler field that counts seconds. Fifteen minutes became fifteen seconds. Five thousand seven hundred sixty rebuilds a day, against the customer’s own API limits. The other two platforms were right. This was the one I’d never run, and the README already said so in a table.

The Linux package shipped its service definitions with the placeholders still in them. Literal text where the interpreter path belonged. Then the installer’s last message told the customer to go and enable them.

My own audit tool lied in the same way. It reported two failures for a file it had never opened. One command built the path. Another read it. Only one of those returns something with a path attached. It didn’t error. It read nothing, and then had opinions.

I wrote a test that failed, and the failure was mine twice over. I’d stubbed the platform name to make a Windows machine take a Linux branch. That breaks the standard library’s own path handling.

The last one is the one I keep coming back to. I wrote five tests for the credential fix, then marked the whole file to skip on Windows โ€” the platform most of the customers are on. The fix for a Windows problem was configured not to run on Windows. I caught it by going back to count how much of it had actually run.

The check has one finding left, and it isn’t a bug. It’s a decision I’ve been putting off since this morning.

Seventeen findings. None of them were in the code.