On 11 August we ran nine parallel red-team reviews against our own admin panel. They found five criticals.
We'll cover the eleven closed findings, but the real story is how each one got past the review that should have caught it.
The state, checked rather than claimed
Before writing any of this we re-checked every finding against the current source. Not against the audit's own status markers, and not against the commit messages saying "fixed": both are claims, and this codebase has a documented history of four separate files asserting states that were false.
Of 12 security findings, 11 are closed. All 5 criticals are closed. One remains open, and we are not saying which — an unremediated finding in a live system is not ours to hand out. It is recorded where it belongs, which is not a public page.
When this guide first went up it said ten closed and two open. One of the two was closed three days after the audit, and it is described below with the rest. We are leaving that visible rather than restating the number as though it had always been eleven, because a count that moves silently is worth less than one that shows its working.
The last one moved for a different reason, and it is the more interesting of the two. It turned out to be two defects under one number: one of them is a change to code, the other is not. The code half is done. Our own re-check looked at the half it could see in the source, found it fixed, and reported the whole finding closed — so for a few hours this guide was going to tell you twelve of twelve. Our own tool, built for this guide, made the same kind of error we're writing about. Twice.
How each one survived review
The bugs themselves are ordinary. What's interesting is how each one looked fine to a reviewer who was paying attention.
A CI service container published to its host. Continuous integration brings up a database for the test run, and a service container publishes its ports to the machine underneath. On a hosted runner that machine is disposable. On a self-hosted one it is a machine you care about, and a database came up reachable with a default password. Every part of that is documented behaviour; the defect was in the composition, which no single file showed.
Quoting a deploy input offered no defence. Because a workflow expression expands before the shell parses the line, the quotes were useless. The fix routes it through an environment variable and validates its shape first.
An unauthenticated endpoint fed an admin page. A public performance beacon accepted a path string. An admin dashboard later built a link from it. Both components were reasonable on their own, but the string crossed a trust boundary that neither file mentioned.
An empty parameter meant "skip", not "deny". Fourteen newsletter routes carried the authentication middleware with no privilege argument, and the middleware treated the absent argument as nothing to check. The routes looked gated because the middleware was there.
The subset guard was on the wrong verb. Assigning yourself a privilege was correctly checked against what you already held. Editing the group you were already in was not, so the check could be walked around without ever assigning anything.
An encoding flag chosen for tidy URLs. Unescaped slashes make structured data read nicely. They also re-enable the sequence that closes a script tag early. It rendered escaped in the admin list, so a reviewing editor saw nothing wrong.
The read path salted and hashed correctly. The write path stored the identifiers raw in the next column over. The correct pattern was already in the file.
A chokepoint that had stopped being one. A sanitising helper covered one column, with a comment describing it as the single point everything passed through. Three more columns had been added since.
The one destructive method was the one without a step-up. Regenerating backup codes — which deletes the old ones first — was also the only method in its controller that did not require re-authentication. It then returned a view that did not exist, so it destroyed the codes and failed.
A supposedly blind request forgery gave back a read. Two background jobs fetched a URL from the database with no host restriction, then wrote the response to a publicly readable path, turning a blind probe into an information leak.
A correct objection stood in for a fix. One log table grew for a year with nothing ever deleting from it. It had not been forgotten: the sweep that covers our other tables names this one and excludes it, and records two reasons. It has no creation timestamp to sweep on, and a size cap would interfere with a check that reads it to decide whether a security policy is safe to enforce. Both reasons were true. But neither was a reason to do nothing, and for twelve months, that's exactly what happened. The fix had to honour the objection rather than ignore it — the sweep now keeps exactly the rows that check depends on, at any age.
The pattern worth taking away
Ten of the eleven weren't coding errors in the usual sense. They were interaction bugs where two correct things became incorrect together — a build runner on a machine you care about, a public beacon feeding an admin dashboard, a middleware default that meant "skip", a stale comment, two good reasons that added up to inaction.
Reviewing a file will not find those. Only asking what crosses between files will.
Our own check was wrong first
The verification script for this guide reported 5 of 12 findings as still open. All five were closed, and every error ran the same direction — toward a false alarm.
It matched on the presence of a port mapping without reading the address it was bound to, when the fix was to bind them to loopback and say so in a comment. It used the wrong filename for a model and reported the finding unverifiable. A shell glob error silently emptied one search, so a guard that exists, is wired into both callers and has its own test suite looked absent. And it asked whether a missing file now exists, when the fix had been to stop asking for that file at all.
Had we believed the first pass, this guide would have announced five open criticals that were fixed a fortnight ago. Each check now tests the guard that actually closed the finding, established by following each remediation commit to the files it touched.
Our own verification failed the same way the production code did. The instrument was reasonable, the target had moved, and only a manual check connected the two.
It then failed again, differently. While updating this guide, the script reported the last open finding closed because the code half was fixed. A finding is closed when all of it is closed; a check that can only see source code will report on source code and say nothing about the rest. It now refuses to close that finding until the other half is remediated and written down.
What to check in your own
Ask where your CI runs and what else lives on that machine. Ask which unauthenticated inputs are displayed to an administrator later. Ask whether an empty middleware argument denies or skips. Ask which sanitising chokepoint has a comment claiming completeness, and count the columns it actually covers.
And when a report says a thing is fixed, check the code rather than the report. We wrote the report, and we still would not take its word for it.
For a concrete starting point, our DMARC and SPF checker will read your domain's mail-sender records, the same ones we found were mostly unenforcing across a set of banks. We also have small-business security in ASEAN for those without a dedicated team, a ten-minute procedure for finding who owns the lookalikes of your domain, and an explanation of what the browser's padlock actually proves.
The lesson from our audit was not in the bugs themselves, which were ordinary, but in why they survived review. Twelve security findings, eleven now closed and all five criticals among them, with one open and deliberately undescribed here. Almost none of them lived in any single file; they lived in the seams between correct components, which is why reading files does not find them and asking what crosses between files does. The same was true of our own verification script, which produced five false alarms out of twelve before we aimed it correctly, and then — checking the fixes for this very update — closed a finding on the strength of the half of it that source can see. A reasonable tool aimed at a target that has moved is a source of error, not truth, and it will keep being one after you have corrected it once.
- Every state in this guide is produced by a script committed alongside it, which re-checks each finding against the current source of this repository. The audit document's own status markers and the remediation commit messages are treated as claims, not evidence.
- ⚠️ ONE FINDING REMAINS OPEN AND IS DELIBERATELY NOT DESCRIBED. No identifier, severity, file, mechanism or location for it appears in this guide, and the committed data file emits it as a count only. An unremediated finding in a live system is not ours to publish, and the guard blocks this guide if it is ever named. The guard's list of forbidden subjects shrinks only when a finding closes, and only by that finding's own terms.
- ⚠️ THIS GUIDE HAS BEEN CORRECTED SINCE PUBLICATION and says so in the body rather than restating its numbers. It first reported ten closed and two open. One closed three days after the audit and is now described here; the other is two defects under one number, of which only the code half is done, so it stays counted as open. The correction is in the guide because a count that changes quietly is worth less than one that shows its working.
- ⚠️ THE VERIFICATION'S FIRST PASS WAS WRONG ON FIVE OF TWELVE, and the guide reports it rather than quietly fixing it. Every error ran toward a false alarm: a port mapping matched without reading its bind address, a wrong model filename, a shell glob error that silently emptied a search, and a check asking whether a file exists when the fix was to stop requesting it.
- ⚠️ AND IT WAS WRONG AGAIN WHILE THIS UPDATE WAS BEING WRITTEN, in the opposite direction — reporting a finding CLOSED because the half of it that lives in source was fixed. That error is the more dangerous of the two: a false alarm gets investigated, a false all-clear gets filed. The check now requires both halves and a written record of the second before it will close that finding.
- ⚠️ SCOPE IS THE SECURITY FINDINGS ONLY. The audit's data-integrity and interface sections are real and are not covered here.
- ⚠️ CLOSED MEANS THE GUARD IS PRESENT IN SOURCE TODAY. It is not a proof of absence of the underlying class, and it is not a penetration test. The guard re-runs on every build, so a finding that reopens turns this guide red rather than leaving it stale.
- The audit was nine parallel red-team reviews of our own admin surface on 11 August 2026, covering interface and information architecture, application security, the authorisation matrix, sitewide injection, authentication and session handling, public attack surface and supply chain, data integrity, operability, and correctness.
This describes findings from an audit of our own systems and their current remediation state. It is not a security assessment of any other organisation, not a penetration-testing methodology, and not a claim that this or any system is free of defects.