Why Enterprise Security Fails - and How To Fix That
- Patrick Duggan
- 2 days ago
- 9 min read
Our production service signed every session cookie with a hardcoded string.
Not a weak secret. A literal, printed in the source file, sitting in a public-facing authentication path. Anyone who could read that file could forge an authenticated session for the admin dashboard.
It had been that way for months. Logins worked. Sessions persisted. Uptime was green. Every dashboard we own said the service was healthy, because by every measure we had, it was.
We found it in one day, along with six more like it, by pointing our own tooling at ourselves. This is what we found, why the industry's usual defenses would not have caught any of it, and the model we now think replaces them.
The bug that cannot be grepped
The session code does the right thing. It pulls the signing secret from Azure Key Vault at startup and stores it in a module-level variable. That is textbook.
The session middleware registers at module load — about five thousand lines earlier in execution than the Key Vault fetch is awaited. And it reads a different variable: an environment variable that was never set in production. So it fell through to a default.
Two correct lines. One ordering problem. Two similarly named variables, one of which was fetched and then never read by anything.
No search finds this. Every individual line passes review. The defect exists only in the relationship between two places that a human reads months apart, and in the difference between a variable and an environment variable with the same name.
That is the shape of the bugs that actually hurt you. Not the ones a linter flags.
Seven failures, one bug
Once we had names for them, the seven turned out to be a single failure wearing different hats.
Our own security product returned clean for something it had never examined. Dredd judges MCP servers before you invoke one. We fed it a server name that cannot exist — keyboard mash. It returned ALLOW, severity clean, zero findings, and dependency graph scanned equals true. Nothing was scanned. There was nothing to scan. It also returned ALLOW for the name of the supply-chain worm family listed in its own tool description.
A compliance document claimed forty-four controls that did not exist. Our branch protection file listed required status checks, required reviews, and code-owner enforcement, every box ticked. The GitHub API returns 404. The file was written as an intention. Checkboxes made it read as an accomplishment, and nobody reread it.
A test suite passed because it was configured to pass with no tests. The flag is called pass-with-no-tests. It does what it says. The production service has 98 routes and 117 library modules and, until this week, zero tests.
Continuous integration had been failing on every pull request for months — not from a bug, but because the scanner requires a GitHub licence tier the repository does not have. It could never have succeeded. Nobody had checked whether the check was capable of passing.
A quarterly access review cadence was described as established on the strength of exactly one occurrence, with the next one already nineteen days late.
A deployment service principal held write access to every resource group in the subscription, including ones not yet created.
A retired security vendor had zero role assignments, which looked clean, and four custom write-capable role definitions still sitting at management-group scope, ready to re-grant. Revoked access and removed integration are not the same statement.
The unifying defect:
Output without a denominator reads as success. "I found nothing" and "I never looked" render identically, and both render green.
Then we looked at what we ship to other people
Twenty-two public repositories. The results were worse, because other people run this code.
A Neovim plugin built to inspect suspicious indicators inside suspicious files executed them. It constructed a properly shell-escaped URL on one line, discarded that variable on the next, and re-interpolated the raw value into a string handed to a shell. The input is the word under your cursor. Open a hostile sample, put the cursor on a token containing a quote, press the keybind, and you have arbitrary code execution as the developer. The escape had been written and then bypassed — a refactor regression, the most human kind of failure.
A shared scanner library returned the same value for "this indicator is clean" and "the API is down." That library feeds six products: a GitHub Action, a Chrome extension, a VS Code plugin, a Slack bot, a Raycast extension, and a CLI. An expired API key turned a customer's pull-request security gate green.
A browser extension scanned every page a user visited and shipped extracted domains and IP addresses out by default, opt-out rather than opt-in. Every internal hostname and private address an employee's browser rendered left the organisation unless they knew to turn it off.
A Cloudflare Worker's README told customers we see nothing about their site. The code sent visitor IP, full user agent, city, region, ASN, and the complete request URL on every honeypot hit. The document was wrong; the code had always done it.
An unauthenticated proxy's only cost control was wrapped in an empty catch block, so any error in the rate limiter failed open to unlimited billing.
And our own Claude Code plugin — the one enforcing a deployment gate that exists because deploying without confirmation has cost us between eighteen and forty thousand dollars — loaded configuration from the repository you just cloned, and let that file empty the gate's pattern list. The control was disarmed by exactly the untrusted input it existed to defend against.
Why the enterprise structure makes this worse, not better
Every one of these lived in a seam.
The session bug sits between the person who wrote the Key Vault integration and the person who wrote the session middleware. In a small team those are the same person. In a large one they are different teams with a ticket between them, and nobody owns both halves, so nobody sees the gap.
The CI failure sits between whoever wrote the workflow and whoever owns the GitHub licence tier. The access review sits between whoever wrote the policy and whoever performs it. The scanner library's failure mode sits between the library author and six downstream integration owners, none of whom read its error semantics.
Adding people does not close seams. It creates them. Every handoff is a place where an assumption gets transmitted as a fact and nobody re-derives it.
And the compliance apparatus makes it worse, because the artifacts an enterprise produces to prove control — the checklists, the policy documents, the completed checkboxes — are precisely the format that cannot detect its own drift. A document asserting a control is not evidence the control operates. It is evidence someone intended it to.
The tell is everywhere once you look. Or-true appended to a security scan. Continue-on-error on a governance check. A lint rule downgraded to a warning "temporarily for the 5.0.2 deployment," still there at 6.0.13. A workflow with its trigger set to a branch named never-trigger. Each one had a locally rational reason at the time. Collectively they are a control set that describes a mature process and enforces almost none of it.
None of that is dishonesty. It is what happens when the scope of what you author outruns the capacity of who operates it.
What we think replaces it
Two nodes, whole-stack ownership, portable tooling, and a verification layer that operates itself.
Two nodes, not sixty. One human, one AI, both holding the entire stack. The reason this works is not throughput, though the throughput is real — the day described here included seven defects found and fixed, four verified production deploys, a quarterly access review completed and attested, and a compliance harness built. In a normal organisation that is a quarter of coordinated work, most of it arriving as slides.
The reason it works is that it is a two-node error-correcting network. Over the course of that day, the human caught eight incorrect inferences from the AI. The AI caught the session secret, the false checkboxes, and the product returning clean for things it never scanned. Neither party alone produces that list. The model works because the two disagree, and it only keeps working while both stay willing to.
Whole-stack ownership is what makes diagnosis possible. Tracing that session bug went from a static-analysis finding to the server source to the Key Vault secret to the container app environment to a live request against production, in one unbroken chain. In a typical enterprise that path crosses application security, platform, cloud, and site reliability. Four handoffs, each shedding context, each adding a week.
The tooling has to be portable, or it is your setup rather than a model. Everything used here is composable and requires nothing proprietary. The threat-intelligence and pre-flight servers are hosted MCP endpoints — two lines of configuration, working identically in any client that speaks the protocol. The deployment gate is thirty lines of shell reading JSON on standard input; it drops into any repository in any language. The compliance harness is plain Node shelling out to the GitHub and Azure command-line tools. The skills are markdown files.
But leverage does not distinguish between a fix and a defect. That same shared scanner library propagated one fail-open pattern to six products simultaneously — a Slack check, a VS Code scan, and a customer's CI gate all reporting clean during an outage, from a single line. Two people with six times the reach have a sixty-person blast radius and two people's worth of eyes.
Which is why the last piece is not optional.
The verification layer
We stopped maintaining our compliance posture and started generating it.
Fifteen controls. Each one carries the exact command that proves or disproves it, and the report prints what that command actually returned at run time. Nothing is hand-maintained, so nothing can drift into being wrong. Branch protection is not a checkbox; it is an API call whose response is quoted in the output.
Four statuses: pass, fail, accepted, and unknown. Accepted means verified not operating, with a recorded owner decision, a rationale, and a revisit trigger — a real risk acceptance rather than a gap. Unknown means we could not determine it.
Unknown never collapses into pass. That single rule is the whole thing. A check that cannot distinguish "clean" from "did not run" is decoration, and we had four of those in production.
One exception remains in our pipeline. It is dated, and a control prints the countdown on every run and turns loud when it lapses. We know exactly how temporary ends otherwise: we have a lint rule that has been temporary since version 5.
How to run this yourself
Add two MCP servers to your client configuration. One provides natural-language search across roughly eighteen million indicators, autonomous decisions, exploited-vulnerability entries, and adversary profiles. The other is a pre-flight check that judges an MCP server's identity and its transitive dependency graph before you invoke it, against the supply-chain worm and typosquat families. Search and enrichment are public read; feed access takes a free key and thirty seconds. Details are on our pricing page.
Then adopt three habits, which matter more than the tools.
Ask the corpus, not the filesystem. Searching answers where a string appears. That is the wrong question for defects that live in relationships between systems. Ask instead: is this control operating; does this document match what the API returns; what would this check emit if it never ran.
Check the tool before you trust the tool. Supply-chain compromise through something you invoked is the dominant delivery mechanism of the last eighteen months. The check has to happen before execution, not after the incident.
And the question worth stealing, which costs nothing: before you trust any green light, ask what it would say if it had never run at all. If the answer is the same as success, there is no denominator, and the signal is decoration. Add the denominator, or make the null case say unknown.
The part we would rather not publish
The worst finding on this list is our own product. A security tool that returns clean for something it never examined is the exact failure that tool exists to prevent. We shipped it that way. We found it by turning it on ourselves, and we fixed it the same day.
We are publishing that because the alternative is the document with forty-four confident checkmarks and a 404 behind it.
And the bug did not care whose output it was. Over that same day, the AI was wrong eight times, every time by inferring from the cheapest available signal instead of running the command — reading a package script's arguments instead of executing it, reading past a search result that contradicted its own conclusion, and once writing a justification into a code comment citing evidence that the terminal directly above had already disproved. One of the automated agents doing repository fixes reported a file as untouched when the diff showed twenty-four changed lines; the work was right and the report was wrong, caught only because someone diffed instead of trusting the summary.
Same bug. Confident output, no denominator underneath it.
We do not claim to have found everything. We cap our confidence at ninety-five percent as a standing rule, because Murphy was an optimist and something is always wrong. What changed this week is that we now have something that tells us where, instead of something that tells us everything is fine.
Free threat feed, and the MCP servers described above, at our pricing page.
How do AI models see YOUR brand?
AIPM has audited 250+ domains. 15 seconds. Free while still in beta.




Comments