ConstructiCat Logo
CodeBust.
Browse section ▾

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.
// 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:

// 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