DEV Community

Yantrix
Yantrix

Posted on AI-assisted

A one-line access check that silently checked nothing

What a real security review finds in a codebase that looked fine.


The setup

We ran Yantrix cold against shadcn-ui/taxonomy — a real, widely-used Next.js + Prisma + NextAuth reference application with almost 20,000 GitHub stars, no special access, no hints, exactly the way a customer's security team would approach a codebase they'd never seen before.

Two functions handle deleting and editing a blog post:

export async function DELETE(req, context) {
  if (!(await verifyCurrentUserHasAccessToPost(params.postId))) {
    return new Response(null, { status: 403 })
  }
  await db.post.delete({ where: { id: params.postId } })
  ...
}
Enter fullscreen mode Exit fullscreen mode

This looks correct. There's a dedicated function, named exactly what it does, called before every mutation. A reviewer skimming this file would move on.

What's actually inside the check

async function verifyCurrentUserHasAccessToPost(postId: string) {
  const session = await getServerSession(authOptions)

  const count = await db.post.count({
    where: {
      id: postId,
      authorId: session?.user.id,
    },
  })

  return count > 0
}
Enter fullscreen mode Exit fullscreen mode

When there's no active session, session?.user.id evaluates to undefined. Prisma's query engine treats a field set to undefined in a where clause as "this field wasn't specified" — not "match nothing." The query that actually runs is:

db.post.count({ where: { id: postId } })
Enter fullscreen mode Exit fullscreen mode

The ownership check is gone. If the post exists at all, count comes back 1, count > 0 is true, and an unauthenticated request is told it has access to someone else's post.

Why this one is worth naming specifically

Every field in this function reads as intentional access control — the name, the placement, the shape of the check. Nothing about it looks like a gap. The failure is entirely in what one ORM does with one specific value, which happens to be the opposite of what the code visibly appears to be checking for. It's exactly the kind of finding that survives a normal code review, because a normal code review is reading for intent, and the intent here was correct — the implementation just didn't do what it looks like it does.

This is also not a one-off. The same shape — a field derived from an optional session, dropped straight into a where clause with no guard in front of it — is common enough across Next.js + Prisma codebases that it's worth a deliberate check for, not something to catch by accident.

The part worth being honest about

This exact bug class turned out to be hard for general-purpose AI reasoning to catch reliably, even for a capable model given the whole file. Across repeated real runs, it required the model to already know a narrow, specific fact about Prisma's own behavior — not a security concept, an ORM implementation detail. When that fact wasn't already primed, the model looked at the code and reasonably concluded the check was fine.

That's the finding that shaped how this is built: this specific pattern is now caught by a deterministic check, not left to chance on whether reasoning happens to recall the right ORM trivia in the moment. Some things need a model that can reason about intent. Some things need a narrow, permanent, always-on check that doesn't forget. Knowing which is which — and building both — is the actual engineering problem, not just running an LLM over a diff.

What this means for a codebase getting ready for scrutiny

A vendor security review, an enterprise procurement questionnaire, or acquisition due diligence isn't looking for typos. It's looking for exactly this shape of thing — code that reads correctly and isn't. The value isn't in flagging more issues. It's in finding the ones that would actually surface when someone official is looking, before they do.


Full methodology — 21 real codebases, 17 risk categories, verified ground-truth recall — at yantrix.ai. The repository's owner has closed both issues and pull requests on this project — respected here rather than worked around. The fix shown above is preserved as a public commit at yantrix-ai/taxonomy, for anyone who wants to verify it directly.

Top comments (0)