All posts
Security
AI

Security in the Age of AI-Written Code

AI assistants made writing code cheap. They did not make reviewing it cheap. What changes about application security when most of your diff was generated — and what a team should actually do about it.

8 min readPatchlight Team · Security

The bottleneck moved. For most of the last two decades, writing the code was the slow part of shipping a feature and reviewing it was the cheap part — a colleague read a hundred lines over coffee and caught the obvious problems. Assistants inverted that. A single afternoon now produces changes that would have taken a week, and the review capacity on the other side of the pull request is exactly what it was before: the same people, the same attention, the same hour between meetings.

That imbalance is the actual security story of AI-assisted development. Not that models write insecure code — humans write plenty — but that they write plausible code faster than anyone can meaningfully read it, and plausible is the specific failure mode that human review is worst at catching.

Why generated code slips past a reader

Human code review evolved around a reasonable assumption: every line had a person behind it, and that person had a reason. When you read a hand-written authorization check, the shape of the code carries signal. Hesitation shows up as awkward naming, a scattered guard clause, an apologetic comment. Reviewers learned to read those tells and slow down where the author clearly did.

Generated code has none of them. It is uniformly fluent. The function that correctly validates a webhook signature and the one that compares it with a non-constant-time equality look equally confident, are named equally well, and carry equally tidy comments. Fluency reads as competence, and reviewers — under time pressure, on their fourth PR of the day — extend it the benefit of the doubt.

What actually goes wrong

Across the findings we see most often on generated diffs, the same handful of patterns keep coming back. None are exotic. All of them are easy to miss at reading speed:

  • Authorization that checks authentication. The endpoint verifies the caller is logged in and never verifies the record belongs to them — the single most common serious finding we report, and one that passes every test written from the same prompt.
  • Credentials inlined to make the example run. Models are trained on tutorials, and tutorials hardcode the key. It gets committed, and then it gets rotated three months later during an audit.
  • Trust inherited from the sample. If the pattern the model learned parsed user input without validating it, the generated version does too — string-concatenated queries, unescaped template rendering, path joins that accept `..`.
  • Dependencies that don't exist. Package names get invented in plausible-looking `import` lines, which is only a build error until somebody registers that name and it becomes a supply-chain attack.
  • Cryptography that compiles. ECB mode, static IVs, a hash where a KDF belongs, an equality check on a MAC. All of it type-checks and none of it is caught by tests.
  • Error handling that swallows the interesting case. A broad `catch` that logs and continues turns a failed permission lookup into a silent allow.

The pattern in all six: the code is wrong in a way that still works. Tests pass, the demo runs, the reviewer has no reason to stop.

The first one is worth seeing in full, because it is the one that reaches production most often and the one that looks most innocent on the way there:

Authenticated is not authorizedA generated handler that passes its generated tests, and the finding it earns. Severity, category and CWE are the fields a Patchlight finding actually carries.
app/api/invoices/[id]/route.ts+14 −0
1export async function GET(req: Request, { params }) {
2 const user = await requireSession(req); // authenticated ✓
3 const invoice = await db.invoice.findById(params.id);
4 if (!invoice) return notFound();
5 return Response.json(invoice);
6}
highsecurityCWE-639route.ts:3

Invoice lookup is not scoped to the caller's organization

requireSession proves who is asking, not what they may read. Any authenticated user can enumerate ids and fetch another tenant's invoice.

  const invoice = await db.invoice.findById(params.id);
+ if (invoice?.orgId !== user.orgId) return notFound();

One line. It is also the line a reviewer skims past, because everything around it is correct — the session check is there, the 404 is there, the naming is good.

Volume changes the math on scanning, too

Reviewing the diff is necessary and no longer sufficient. When a codebase grows by 40% in a quarter, a scan of the code nobody touched this week is a different scan than it was last quarter — the file that was fine in March is now called from four new places, two of which pass user input to it. Diff-scoped review, by construction, never re-examines that file.

Review reads the diff. Risk doesn't stay in the diff.Schematic — each square is one file in a service. Amber squares changed in the last 30 days; ringed squares carry an open finding. A diff-scoped review can only ever look at the amber ones.
changed in the last 30 days — what PR review seesfile carrying an open finding

This is why we treat whole-repository security monitoring as separate from PR review rather than a premium version of it. They answer different questions. Review asks whether this change is safe to merge. The monitor asks whether the codebase, as it stands today, is safe — including everything that was merged back when the reviewer had more time.

What a team should actually do

  • Treat every diff as untrusted input, regardless of author. The provenance of a line — human, assistant, or copy-paste from an old branch — should not change how carefully it is checked. Making that uniform also removes the awkward politics of reviewing a teammate's AI output more suspiciously than their handwritten code.
  • Make the first pass automatic. Any check that depends on someone remembering to run it gets skipped in exactly the week it was needed. Automated review that comments on every PR sets a floor that deadline pressure cannot lower.
  • Scan the whole repository on a schedule. Weekly or nightly, against your default branch, so the findings that predate this sprint still surface.
  • Pin and verify dependencies. Lockfiles, integrity hashes, and a human glance at anything newly added — especially packages introduced by a generated import line.
  • Keep accountability human. Automation should raise the floor on what gets caught, not move responsibility off the person who clicks merge. Someone still owns the change.

Where we fit

Patchlight runs the first pass so your reviewers can spend their attention on design and intent instead of scanning for injection sinks. Every pull request and commit gets reviewed automatically, with findings posted as inline comments where the code is. A scheduled security monitor scans the full repository on its own cadence and reports what the diff view never shows.

We are deliberately unopinionated about how the code got written. Assistant-generated, hand-typed, or pasted from an internal wiki — it all goes through the same checks, because the vulnerability doesn't care either.