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