Security Blind Spot.
AI assistants emit functional code that silently omits the security controls a human would add by habit — input validation, authorization, output encoding, secrets handling — because "it compiles and returns 200" looks like done.
##Signs and Symptoms
A reviewer spots a Security Blind Spot when the code works on the happy path but the controls that a security-aware human adds reflexively are simply absent. The model demonstrates the feature, not the defense. Tell-tale signs:
- String-concatenated queries / commands instead of parameterized ones (SQL/command/LDAP injection sinks).
- User input flows to output with no encoding (reflected/stored XSS) or to
fs/child_process/fetchwith no allow-list (path traversal, SSRF). - The endpoint authenticates but never authorizes — it checks who you are, never whether you may touch this record (IDOR / broken object-level auth).
- Hardcoded secrets — API keys, tokens, DB passwords inline "to make it run."
- Missing-by-omission controls: no CSRF token, no security headers, no rate limit, weak/legacy crypto (MD5, SHA-1,
Math.random()for tokens). - Comment theater: a
// validate inputor// TODO: add authcomment sits where the actual check should be. OX Security found excessive inline comments ("Comments Everywhere") in 90–100% of the AI-generated code it studied; in this smell such a comment often stands in for the missing logic.
// AI-generated Express handler — looks complete, ships four holes
app.get("/api/orders/:id", authMiddleware, async (req, res) => {
const apiKey = "sk_live_8f3b2a91c7d44e0f"; // 1. hardcoded secret
const { rows: [order] } = await db.query(
`SELECT * FROM orders WHERE id = ${req.params.id}` // 2. SQL injection
);
res.send(`<h1>Order for ${order.customerName}</h1>`); // 3. reflected XSS
// 4. IDOR: authMiddleware proves login, never checks order.userId === req.user.id
});
Four distinct vulnerabilities, zero failing tests — the smell is what isn't there.
##Reasons for the Problem
Why models produce it
- The training target is plausibility, not safety. Next-token prediction optimizes for the most common way code is written, and public corpora are saturated with tutorial-grade, concatenated-query, no-authz snippets. OX Security's post on vibe-coding security puts it plainly: "String concatenation appears most frequently in training data." In Carnegie Mellon's SusVibes benchmark of 200 real-world feature tasks, SWE-Agent with Claude 4 Sonnet produced functionally correct solutions about 61% of the time but secure ones only about 10.5% of the time.
- "Done" is defined by the happy path. An LLM optimizes to make the visible request succeed. Security controls produce no positive signal in a quick run — a missing check fails no test and breaks no demo — so they get dropped.
- No repo context. The model can't see your
authorize()helper, your validation schema, or your secrets manager, so it inlines a literal key or skips the check rather than reusing an existing control. - Sycophancy + velocity. It returns the thing you literally asked for ("add an order endpoint"), not the threat model around it, and bottlenecks like review and threat-modeling are exactly what AI-speed development removes. OX Security's core finding is that AI-generated code does not contain more vulnerabilities per line than human-written code; what ships vulnerable code to prod is velocity, because "proper code review simply cannot scale to match the new output velocity." OX also warns of apps that are "insecure by dumbness": built and deployed with AI by non-technical users who lack the security expertise to notice what is missing.
- Training-cutoff staleness. Models reach for crypto and library patterns that were fine years ago (MD5, SHA-1, deprecated TLS options, old auth flows).
Why it hurts
- Direct exploitability. Veracode's 2025 GenAI Code Security Report (80 coding tasks, 100+ LLMs) found security flaws in 45% of cases, and the models failed to defend against cross-site scripting (CWE-80) in 86% of cases. CodeRabbit found XSS vulnerabilities 2.74× more often in AI-co-authored pull requests than in human-only ones; Tenzai found that none of 15 apps built by five coding agents had CSRF protection or set security headers; Escape.tech found 400+ exposed secrets across 5,600+ vibe-coded apps.
- Invisible to tests and to readers. A missing authorization check has no failing test and no diff to point at, so it sails through review — and AI's output volume means review can't scale to catch it.
- Compounding tech debt. GitClear's 2025 analysis of 211 million changed lines found refactoring-associated ("moved") lines falling from 25% of changed lines in 2021 to under 10% in 2024, while copy/pasted lines rose from 8.3% to 12.3% and in 2024 exceeded moved lines for the first time; blocks with five or more duplicated lines grew roughly eightfold during 2024. The same insecure pattern gets cloned across the codebase, multiplying the blast radius of any single blind spot.
##Treatment
Review & prompting tactics
- Name the threat model in the prompt. "Write this endpoint assuming the
idis attacker-controlled; enforce object-level authorization, parameterize all queries, encode all output, and read secrets fromprocess.env." Specifying the adversary flips the default. - Force reuse of existing controls. "Use our
authorize(user, 'order', id)helper and theorderSchemavalidator — do not write new validation." This directly counters the no-repo-context cause (and the related Duplicate Code smell). - Make the model run the tools. Require it to run
semgrep --config p/owasp-top-ten,gitleaks detect,npm audit, and youreslint-plugin-securityconfig, then fix what they flag, before declaring done. - Ask for the negatives explicitly. "List the authN, authZ, input-validation, output-encoding, and secrets concerns for this handler and show where each is enforced." A blank in that list is the bug.
- Threat-model the diff, not the line. The dangerous holes are omissions, so review by asking "what control should be here and isn't?" rather than scanning for bad lines.
The refactor — parameterize the query (kills injection), encode output (kills XSS), add an object-level authorization check (kills IDOR), and move the inlined secret into configuration:
// AFTER
app.get("/api/orders/:id",
authMiddleware,
validate(orderParamsSchema), // input validation
async (req, res) => {
const { rows: [order] } = await db.query(
"SELECT * FROM orders WHERE id = $1", [req.params.id] // parameterized
);
if (!order || order.userId !== req.user.id) { // object-level authz
return res.sendStatus(404);
}
res.json({ customerName: order.customerName }); // structured output, auto-encoded
}
);
// secret: process.env.STRIPE_API_KEY, loaded once at startup, never inlined
Where the smell has spread by cloning, fix it once and apply Extract Method to the control (requireOrderOwnership, escapeHtml) so every call site shares one audited implementation instead of N copies.
##Detected by
- Gitleaks generic-api-key — Hardcoded secrets / generic-api-key
- Semgrep p/owasp-top-ten — p/owasp-top-ten (e.g. sql-injection, xss, dangerous formatstring sinks)
- GitHub CodeQL js/sql-injection, js/xss, js/hardcoded-credentials, js/path-injection
- eslint-plugin-security detect-non-literal-fs-filename, detect-child-process, detect-eval-with-expression, detect-object-injection
- Bandit (Python) B105 / B608 / B324 — B105 hardcoded_password_string, B608 hardcoded_sql_expressions, B324 weak hashlib
- SonarQube / SonarSource Security Hotspots / Vulnerability — Security Hotspots & Vulnerability rules (injection, weak cryptography, hardcoded credentials)
- npm audit / OWASP Dependency-Check npm audit / OWASP Dependency-Check — Known-vulnerable dependency detection