Skip to content

Resources

Six systems passed their tests. None was safe to deploy as shipped.

By Mikko Laakkonen · Taiga co-founder and CEO

Published · Updated

A passing test suite proves the features work, not that the trust model holds. The same variance that moved authentication and dependencies between runs also moved the security boundary — and a functional acceptance gate never looks there.

This is Part 2 of One spec in. Six systems out. Four of the six AI-built invoice services let an unauthenticated caller create an approver account. Five let one account reach another person's invoice. All 71 named tests still passed.

We had published the experiment seven days earlier with a sentence calling a hardcoded fallback secret “the one real finding.” The sentence was wrong.

The second review read every production file, reran the current dependency audits and all six test suites, then followed the authorization paths for invoices, PDFs, personal data, and audit records. We exercised representative, load-bearing findings against the running code.

One FastAPI run accepted an approver account from an unauthenticated request. A different submitter could then open another person's invoice, read and erase its counterparty email, and list the audit table. The Claude run with dev-insecure-secret accepted a token signed with that published fallback. Another Claude run accepted an amount of 0.001, rounded it to zero, hit a database constraint inside an asynchronous upload callback, and terminated the process. The failed request left the uploaded file behind.

The code had not changed since the first article. We had asked a better set of questions.

The two-person rule passed

The frozen specification defined two roles. Submitters create invoices, approvers decide them, and anything over EUR 10,000 needs two distinct approvers. Nobody may approve their own invoice. It did not say how a person receives either role.

Four separate runs invented a public registration endpoint that accepted the role in the request body. A caller could send role=approver twice and acquire two user IDs. The service then treated those IDs as two trusted people.

The approval code was good. Every run blocked self-approval. Every run enforced distinct IDs above the threshold, often with a database uniqueness constraint. The workflow control passed exactly where we had placed it. Identity provisioning sat outside the specification, so the control could be defeated before the workflow began. This happened in two Claude Code runs and two Copilot CLI runs, across different languages, session designs, and dependency choices. Their missing identity boundary did not differ.

The audit log recorded every read

Five systems had clear object-level authorization failures. Once a caller had any valid account, changing an invoice ID was enough to reach records belonging to someone else. The exposed data varied by run: invoice metadata, supplier contact details, PDFs, audit histories, and actor identities. Most affected systems also let the caller erase another invoice's contact email.

All six systems correctly logged reads of that email. The spec explicitly required it. Most did not first check whether the reader was entitled to see the address. An ordinary account could read somebody else's personal data, and the audit table would record it. The logging requirement passed. The access policy had never been written.

That distinction matters in regulated software. A log tells an auditor that an event happened. Authorization decides whether it should have happened.

The scanners did what we asked

The three Node lockfiles returned zero known vulnerabilities in a current dependency audit. One Copilot run did have directly reachable denial-of-service issues in its pinned multipart parser; the other two Python systems avoided third-party packages but used standard-library reference servers with no limits for slow or oversized bodies.

The existing Semgrep run reported informational CSRF notes and six raw-SQL errors. The SQL findings were false positives: fixed clause fragments with properly bound values. Gitleaks reported no secrets and missed both active fallback strings.

None of this makes those tools useless. They confirmed clean SQL binding, current Node dependency trees, and sound password primitives across the six systems. They did not model who may create a privileged identity, who owns invoice 17, or who may erase the email inside it.

The ranking, worst to best

The full review scores each run on a comparative threat scale from 0 to 100, where higher is worse. It is not CVSS, and the small differences in the middle are not statistically meaningful. The useful result is that the best draw was still not deployable as shipped.

RunThreat scoreWorst reason
Claude Code, run 292Public approver creation, a published fallback JWT secret that permits token forgery, broad invoice/PDF/PII access, stored XSS
Copilot CLI, run 187Public approver creation, cross-user PII erasure, unbounded upload buffering, currently vulnerable multipart dependencies
Copilot CLI, run 385Public approver creation, global PDF/PII/audit access, an unbounded single-threaded request server
Claude Code, run 376Public approver creation plus known privileged accounts, a hardcoded session fallback, a development-only in-memory session store
Claude Code, run 167Cross-user PII access and erasure, known credentials on an all-interface listener, a verified process-terminating input path
Copilot CLI, run 264Best only comparatively: known privileged credentials, full cross-user PDF/PII disclosure, unbounded threaded request handling

The complete ranking, scoring rubric, and per-run detail are in the full security report, not in an agent leaderboard. We do not compare the two products against each other; the finding is about what a coding agent is, not which one to buy.

A longer specification would still have crashed

The next version of the invoice specification should close the obvious gaps. Identity must come from a trusted administrator or identity provider. Every invoice, PDF, personal-data, and audit operation needs an explicit role and ownership policy. Erasure needs separate authority. Sessions need expiry, secrets need to fail closed, and request bodies need limits.

Writing those decisions down would have prevented most of the repeated failures. It would not have caught everything. Claude run 1's sub-cent crash is the useful counterexample. The requirement already said the amount must be positive. The route checked for a positive number. A later conversion rounded the value to zero, and the database rejected it in a place the web framework did not catch.

The decision existed. The hostile boundary test did not. For generated software, a security decision needs a corresponding implementation and evidence from the exact build that will ship. Regeneration produces a new build, so the evidence has to be produced again.

Taiga has to pass the same test

Taiga does not promise identical generated code. Its documented workflows connect project context, initiatives, implementation and review. The current capability reference states the prerequisites and limits; the agreed scope and the evidence for the exact run matter more than a general claim of repeatability.

Architecture, data flow, threat model, DPIA input, and risk register exist before a Taiga build. Their presence alone proves nothing. They have to decide how privileged identities are provisioned, which records each role may reach, who may erase personal data, and where resource limits apply — and those decisions only count if executable negative tests hold them to the exact delivery, not to the process that produced it.

That is the standard anyone should hold us to. A description of how we work is not evidence; the record attached to a specific build is. The safe version of this also separates the builder from the security judgment: the reviewer needs a different job, explicit policy, and permission to fail the delivery, with the pass or failure attached to that exact run.

We stopped too early

The first article got the variance result right. Reviews attach to one draw and do not transfer automatically to the next. The scanners missed the fallback secret and raised false SQL alarms. But it underestimated the security problem, because we found the secret we had planted and stopped too early.

The fuller review is still limited. Six small services from two agent products are not a benchmark. The threat scores are comparative judgments. We did not test production infrastructure that does not exist in the repository. Someone else may find more. That is why the code, method, report, and correction stay public, and our own governance claims should be tested the same way.

The first article asked what the agent would build the second time. After this review, I would ask for the security record attached to that second build. Then I would try to break it.

Sources

Frequently asked questions

If the six AI-built systems passed their tests, why weren't they safe to deploy?+

The tests checked the functional specification, not hostile-user boundaries. All 71 named tests passed, yet four of the six systems let an unauthenticated caller create an approver account, and five let one account reach another person's invoice. A passing functional suite says the features work, not that the trust model holds.

What was the most common failure across the six systems?+

Missing authorization. The spec defined two roles but did not say how a person receives one, so four runs invented a public registration endpoint that accepted the role in the request body. Five runs also confused authentication with authorization: once a caller had any valid account, changing an invoice ID was enough to read or erase someone else's records.

Did the security scanners catch these problems?+

No. Semgrep and gitleaks confirmed clean SQL binding, current dependency trees, and sound password hashing, but they did not model who may create a privileged identity or who owns a given invoice. The worst findings were authorization and trust-model failures, which the scanners are not built to see.

Does this change the conclusion of the first article?+

It corrects it. Part 1 called a hardcoded fallback secret “the one real finding.” A deeper source-assisted review of the same frozen code found that was wrong: none of the six systems was safe to deploy as shipped. The variance result held; the security assessment stopped too early.

Would a longer, more detailed specification have fixed this?+

It would have closed most of the repeated gaps but not all of them. Writing down identity provisioning, per-object authorization, and resource limits prevents the common failures. It would not have caught a sub-cent amount that rounded to zero and crashed one system: that decision existed in the spec, but the hostile boundary test did not. Security decisions need matching implementations and evidence from the exact build that ships.

Run the six-question self-test

See how a requirement or a repository finding becomes a reviewed change.