Skip to content
Underdog Diary

Lock It Down Before You Ship

'Check for security issues' is a request with no target. The agent picks what to check, reports on what it picked, and marks the task done.

  • Lesson 07
  • Advanced
  • 12 min
  • Updated: August 2026

Before this: Lessons 5-6, and something you've actually built.

The vulnerability list every security team references got an update at the end of 2025, and two categories on it are brand new. Here's exactly what breaks in a product built by an AI agent, with an audit prompt for every failure class and the terminal commands to check it yourself.

First, a quick note on why "check for security issues" as a one-line prompt doesn't work. It's a request with no target. The agent picks what to check, reports on whatever it picked, and marks the task done. You need to audit by category, and the categories already exist.

The List Got an Update

OWASP Top 10:2025 is built on data from more than 2.8 million applications across thirteen organizations. Eight categories come straight from that data. The other two came from a practitioner survey — the stuff security engineers already catch by hand that scanners still miss.

Ranked by risk:

  1. Broken Access Control
  2. Security Misconfiguration
  3. Software Supply Chain Failures — new
  4. Cryptographic Failures
  5. Injection
  6. Insecure Design
  7. Authentication Failures
  8. Software and Data Integrity Failures
  9. Logging and Alerting Failures
  10. Mishandling of Exceptional Conditions — new

Full list: https://owasp.org/Top10/2025/

What changed since 2021: misconfiguration jumped from fifth place to second. Vulnerable components stopped being their own line item and expanded into the full supply chain category. SSRF dissolved into other categories. And a brand-new one showed up — what happens when something goes wrong and nobody planned for it.

Broken Access Control

Top of the list, by a wide margin. Some form of it showed up in 100% of tested applications, with a max prevalence of 20.15%. This is the single most likely hole in whatever your agent just built for you.

The pattern repeats everywhere. The agent adds a login check, confirms the user is authenticated, and calls the job done. It never checks whether the object being touched actually belongs to that user.

  • /api/order/1043 returns your order. Swap in 1044 and you get someone else's.
  • The admin button is hidden in the UI, but the endpoint behind it is wide open to anyone who finds the URL.
  • One extra field in a request body bumps your own role to admin.
  • The free-tier limit lives on a button in the frontend. The handler never counts it.

The common thread: the protection lives in the interface. Anything that's only drawn on screen doesn't exist for an attacker — they just hit the endpoint directly.

Prompt for your agent:

Audit access control.

For every handler, list in a table: who can call it, whose object it touches, and where in the code ownership gets verified.

Separately, flag every place where the check only exists on the client.

Separately, check which model fields get accepted straight from the request body without a whitelist.

Don't fix anything. Give me the table and a list of holes with line numbers.

Keep those last two lines. Without them, the model starts patching as it goes, and you get a diff instead of a picture of what's actually broken.

Supply Chain Failures

The newest category, and the most underrated one. In the practitioner survey it ranked first — half the respondents put it at the top. Enough happened in the last year to explain why.

September 2025 — the first genuinely self-propagating worm in npm. After install, the payload grabbed npm tokens, cloud keys, and SSH keys, found other packages by the same author, injected itself into them, and republished. No human involved.

August 2026 — same mechanism, new target: developer tooling configs. Claude Code and Cursor config files, model API keys. The payload spread through more than four hundred packages, and it persisted through Claude Code hooks.

March 2026 — malicious versions of axios lived in the registry for roughly three hours and dropped a trojan across all three major operating systems. The entry point wasn't a registry breach — it was a compromised conversation with a maintainer.

There's also a failure mode that showed up specifically because of agents: the model confidently recommends installing a package that doesn't exist. Out of 2.23 million generated code samples, nearly one in five referenced a hallucinated package name. Someone else registers that name afterward. In one test, five models independently hallucinated 127 identical package names, and 53 of them were still available to claim. Read every install command an agent gives you before you run it — check whether that package existed yesterday.

Prompt for your agent:

Break down the project's dependencies.

For everything in package.json, tell me: why it's here, who maintains it, when it last released, and how many dependencies it pulls in on its own.

Separately, flag anything added in the last month.

Separately, flag anything that could be replaced with ten lines of your own code.

Don't remove anything. Just give me the list.

Secrets

28.65 million new hardcoded secrets leaked to public GitHub in 2025 — up a third year over year. The share of commits containing a secret is measured separately: 1.5% across public GitHub overall, 3.2% for commits made with an AI assistant. Twice as often.

How long a leaked key survives is measured too. In a honeypot test, the first unauthorized use attempt landed eleven minutes after the push. Five more attempts followed over the next two hours, from four different countries.

Deleting the commit isn't enough, and that's just how GitHub works. A commit stays reachable by its hash across the fork network even after deletion, a revert, and a force push. The order matters, and it's the opposite of what feels natural: revoke and rotate the key first, clean up the history second.

Run this against the full history, not just current files:

gitleaks detect --source . -v

Trust in Input

Injection dropped from third place to fifth, which doesn't mean it got less common. The spread across injection types is enormous, and model benchmarks show exactly why: generated code holds up against SQL injection 80% of the time, and against cross-site scripting only 13% of the time.

The reason is mechanical. For SQL, the safe answer is always the safe answer — a parameterized query is correct no matter where the data came from. For markup output, correctness depends on context — exactly where that string lands — and without that context the model just ships whatever compiles first.

  • User text gets dropped into markup without escaping for the specific place it renders.
  • A payment webhook gets accepted without checking its signature — the payment gets forged with a plain request.
  • The server fetches a file from a user-supplied URL, and that URL points back into your own internal network.
  • An avatar upload gets checked by file extension, and the file itself is executable.

Prompt for your agent:

Find every place data comes in from outside: forms, URL parameters, file uploads, webhooks, responses from third-party services.

For each one, tell me: what gets validated, exactly where, and what happens with a value it doesn't expect.

Separately, check every webhook: does the signature get verified before the data touches the database?

Give me the list. Don't fix anything.

Race Conditions and Limits

Two requests land at the same time and the code runs twice: a balance gets spent twice, a promo code activates more than once, a free-tier limit gets bypassed with parallel calls.

This is the least obvious category, because under normal use everything looks correct. It only breaks under load, or when someone triggers it on purpose.

Prompt for your agent:

Find every operation where it matters that the action happens exactly once: a charge, an activation, a credit, a balance change.

For each one, tell me what happens with two simultaneous requests, and what's actually preventing it: a unique constraint, a database lock, or an idempotency key.

Separately, list every endpoint with no rate limit.

Attack Vectors Specific to Bots and Scrapers

This part doesn't come from OWASP. It's from building things like Seika, a Telegram-based task tracker, and running into these directly.

Telegram IDOR and FSM poisoning. Hiding buttons is pointless in a bot, same as it is on the web. An attacker can craft callback_data by hand or intercept someone else's. If your FSM state handler trusts that payload blindly instead of checking it against message.from_user.id from a trusted context, it will happily modify an object that isn't yours to modify.

NoSQL injection (MongoDB). Injection didn't go away, it just changed syntax. Passing an object like {"$ne": null} instead of a plain string into an endpoint can bypass a password or subscription check if the driver isn't configured for strict input typing.

Scraper poisoning (Playwright and friends). HTML pulled from a scrape — a marketplace listing, a competitor's page — often gets treated as trusted internal data. Render that content unsanitized in a React admin panel and you've got XSS that can hijack an admin session.

Prompt for your agent, bots:

Review every Telegram handler and FSM state. List every place where callback_data or free-text input changes database state without a hard check against the initiating user_id.

Prompt for your agent, APIs:

Review the DTOs and validation schemas — Pydantic, Zod, whatever you're using. Find every endpoint that accepts an arbitrary JSON object without a strictly defined structure and field types.

What the Benchmarks Actually Show

Models pick the insecure implementation in roughly 45% of tasks — 80 tasks, over a hundred models, prompts with zero mention of security on purpose. That number hasn't moved in two years: 55% pass rate in 2024, same today, even as raw syntactic correctness climbed close to its ceiling.

A manual pentest of fifteen production apps, built with five different tools, turned up 69 exploitable vulnerabilities. The 100% failure rates matter more than any single count here: not one of the fifteen apps had CSRF protection or security headers in place.

Telling the model to "write securely" helps, but only sometimes. Reasoning models showed a real improvement when security was mentioned in the prompt. Standard models showed basically none. The instruction isn't a substitute for the audit.

What to Actually Run

About twenty minutes, start to finish:

npm audit --omit=dev
# known vulnerabilities in dependencies
 
npx osv-scanner scan source -r .
# same thing, against the OSV database
 
gitleaks detect --source . -v
# secrets across the full commit history
 
npx nuclei -u https://your-site.com
# known CVEs and exposed admin panels

Plus four checks by hand that need neither an agent nor a tool. Open your product in an incognito browser, logged out, and walk through the URLs. Swap an ID in the address bar for the one next to it. Send the same request twice in a row. Search your own commit history for your own keys.

If you're running on a hosted backend like Supabase, row-level security has to be on for every table exposed externally. Its absence is exactly what handed one vibe-coded platform a 9.3-severity vulnerability — the generated schemas shipped with no policies at all, and anyone could read or write anyone else's tables.

What Automation Won't Catch

The boundary here is honest and worth knowing up front. Scanners find known patterns: an outdated dependency, a missing header, a familiar injection shape. They don't understand logic.

Bypassing a business rule through a legal sequence of actions, reaching someone else's object through a guessed ID, redeeming a promo code twice through two different paths — every scanner passes all three green. That's exactly what happened with a login bypass built on a single ID sitting in an open config file: two endpoints just didn't require auth, and no automated tool called that a hole.

The Full Checklist

  • Every handler has been checked for whose object it returns
  • Model fields get accepted through a whitelist, role never gets read from the request body
  • Row-level security is on for every table exposed externally
  • Keys live in environment variables, commit history has been run through a secret scanner
  • Any key that's ever leaked has been revoked and rotated — deleting it from files isn't enough
  • The dependency list has been read by a human, anything added recently has been checked for whether it actually exists
  • Webhook signatures get verified before anything touches the database
  • Money-moving operations are protected against running twice
  • Debug mode is off, detailed errors don't leak to the outside
  • callback_data payloads in bots carry no sensitive IDs — or those IDs get validated for legitimate access
  • Critical transactions (granting access, crediting a balance) are wrapped in a lock
  • Incoming data from webhooks and scrapers is treated as untrusted and validated against a strict schema
  • Security headers are set, CSRF protection is on
  • Python dependencies are pinned with hashes, not just the Node.js ones

Info here is current as of August 2026. This space moves in weeks, not years — a package that's clean today can be compromised by the time you read this. Run the commands yourself, read the output yourself, don't take my word for any of it.

DYOR. Same abbreviation crypto uses, same reason. Universal blueprints are fiction. Your feedback is in the wreckage.

New lessons, when they’re ready

No schedule, no drip campaign. I send one when I've actually learned something worth writing down.