---
title: "Security Blind Spot"
type: "ai-smell"
slug: "security-blind-spot"
url: "http://localhost:3000/en/ai-smells/security-blind-spot.md"
category: "Correctness"
description: "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."
---
# 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`/`fetch` with 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 input` or `// TODO: add auth` comment sits where the actual check should be — OX found redundant comments in 90–100% of AI code, often standing in for the missing logic.

```js
// AI-generated Express handler — looks complete, ships three holes
app.get("/api/orders/:id", authMiddleware, async (req, res) => {
  const apiKey = "sk_live_8f3b2a91c7d44e0f";            // 1. hardcoded secret
  const 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
  // note: authMiddleware proves login, never checks order.userId === req.user.id (IDOR)
});

```

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 notes models "default to insecure approaches because they're most frequently in training data." Carnegie Mellon found 61% of AI snippets _function_ but only \~10.5% pass security review.
* **"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, so they get dropped — what OX calls _"insecure by ignorance": functional code with missing safeguards because no one involved knew what was required._
* **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's core finding is that _velocity_, not per-line defect rate, is what ships vulnerable code to prod.
* **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.** Independent studies cited by OX: \~62% of AI-generated code ships with a vulnerability; 86% failed XSS defenses (CSET/Georgetown); CodeRabbit measured 2.74× more XSS than human code; Tenzai found 0/15 AI-built apps set security headers or CSRF protection; 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 data (refactoring down from 25% to <10% of changed lines, copy/paste up and now exceeding moved lines, duplicate blocks up \~8×) means 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 `id` is attacker-controlled; enforce object-level authorization, parameterize all queries, encode all output, and read secrets from `process.env`." Specifying the adversary flips the default.
* **Force reuse of existing controls.** "Use our `authorize(user, 'order', id)` helper and the `orderSchema` validator — 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 your `eslint-plugin-security` config, 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 _Extract_ the inlined secret to config:

```js
// AFTER
app.get("/api/orders/:id",
  authMiddleware,
  validate(orderParamsSchema),                    // input validation
  async (req, res) => {
    const 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 **Extract Function** the control (`requireOrderOwnership`, `escapeHtml`) so every call site shares one audited implementation instead of N copies.

## Detected by

- **Gitleaks** `hardcoded secrets / generic-api-key` (https://github.com/gitleaks/gitleaks)
- **Semgrep** `p/owasp-top-ten (e.g. sql-injection, xss, dangerous formatstring sinks)` (https://semgrep.dev/p/owasp-top-ten)
- **GitHub CodeQL** `js/sql-injection, js/xss, js/hardcoded-credentials, js/path-injection` (https://codeql.github.com/codeql-query-help/javascript/)
- **eslint-plugin-security** `detect-non-literal-fs-filename, detect-child-process, detect-eval-with-expression, detect-object-injection` (https://github.com/eslint-community/eslint-plugin-security)
- **Bandit (Python)** `B105 hardcoded_password_string, B608 hardcoded_sql_expressions, B324 weak hashlib` (https://bandit.readthedocs.io/en/latest/plugins/index.html)
- **SonarQube / SonarSource** `Security Hotspots & Vulnerability rules (injection, weak cryptography, hardcoded credentials)` (https://rules.sonarsource.com/javascript/type/Vulnerability/)
- **npm audit / OWASP Dependency-Check** `known-vulnerable dependency detection` (https://owasp.org/www-project-dependency-check/)
