AI-Generated Code and AppSec: The Blind Spots Your SAST Tool Won't Catch

Over 90% of development teams now use AI coding assistants. But AI-generated code introduces a new class of security blind spots — subtle, plausible-looking code that passes standard SAST scans while embedding vulnerabilities that a senior developer would catch on review. Here is what to look for and how to build checks that actually work.

AI coding assistants are now embedded in most development workflows. GitHub Copilot, Cursor, Claude, and similar tools generate code that looks correct, passes type checking, and in many cases works exactly as intended. AppSec teams at organisations that have surveyed their tooling report that AI-assisted code now accounts for a significant fraction of new commits — some putting it above 40% of all new lines.

The problem is not that AI generates bad code in the obvious ways SAST tools catch. It is that AI generates subtly wrong code: plausible, working code that contains security properties the developer did not ask for and the AI did not mention. This article covers the main categories of security blind spots introduced by AI code generation and what detection and review processes to add.

Category 1: Unsafe Defaults and Omitted Hardening

AI models generate the most common pattern for a given task — and the most common pattern is often the one that works, not the one that is secure. Security hardening requires explicit instruction, and most developers do not include security requirements in their prompts.

Python — subprocess without shell=False

A developer prompts: “write a function to run a shell command and return output”. A plausible AI response:

import subprocess

def run_command(cmd: str) -> str:
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout

This works. It also enables shell injection if cmd is ever constructed from user input. The correct pattern:

import subprocess
import shlex

def run_command(cmd: str) -> str:
    result = subprocess.run(shlex.split(cmd), shell=False, capture_output=True, text=True)
    return result.stdout

SAST tools will not flag the first version unless cmd is demonstrably tainted — and in the context of an isolated utility function, the taint path is not visible.

JavaScript — eval and innerHTML

AI frequently reaches for innerHTML when generating DOM manipulation code. A prompt asking to “update the notification banner with the server message” may produce:

document.getElementById('banner').innerHTML = serverMessage;

The secure alternative is always textContent for text data, or structured DOM construction for HTML. SAST tools flag explicit eval() reliably, but innerHTML flagging depends on context and is routinely suppressed as a false positive in codebases that legitimately use it in controlled contexts.

Go — insecure TLS configuration

Go’s TLS configuration has secure defaults in the standard library, but AI often generates code that explicitly overrides them when the developer asks for “a simple HTTPS client”:

tr := &http.Transport{
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}

InsecureSkipVerify: true is present because it solves the common development problem of self-signed certificates. It disables all certificate verification. It should never ship to production but frequently does in AI-generated integration code.

Category 2: Incorrect Cryptographic Choices

AI models are trained on a corpus that includes code from across the last 15+ years. Old cryptographic patterns (MD5, SHA-1, AES-CBC without authenticated encryption, ECB mode, insufficient iteration counts for password hashing) appear in that corpus and get reproduced.

# AI-generated password hashing — looks functional, is weak
import hashlib

def hash_password(password: str, salt: str) -> str:
    return hashlib.sha256(f"{salt}{password}".encode()).hexdigest()

The correct pattern uses a memory-hard KDF:

import hashlib
import os

def hash_password(password: str) -> tuple[str, str]:
    salt = os.urandom(32)
    key = hashlib.scrypt(password.encode(), salt=salt, n=16384, r=8, p=1)
    return key.hex(), salt.hex()

SAST rules for weak cryptography exist but often require specific function name matching. Custom implementations using low-level primitives with incorrect parameters are missed.

Category 3: Verbose Error Responses That Leak Internal State

AI generates helpful error messages because it is trained on code where helpful error messages improve developer experience. The model does not distinguish between development-mode and production-mode error handling.

// AI-generated Express error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    error: err.message,
    stack: err.stack,
    query: req.query,
    body: req.body
  });
});

This pattern, shipped to production, exposes stack traces (revealing file paths, dependency names and versions), reflects request parameters back to the client, and provides an attacker with detailed information about the application’s internal structure.

Category 4: Race Conditions in Concurrent Code

AI generates code that is functionally correct under single-threaded assumptions. When the same code runs in concurrent contexts — web server handlers, async event loops, multi-process task queues — subtle race conditions emerge.

# AI-generated inventory check — has TOCTOU race
def purchase_item(user_id: int, item_id: int) -> bool:
    item = db.get_item(item_id)
    if item.stock > 0:
        db.decrement_stock(item_id)
        db.create_order(user_id, item_id)
        return True
    return False

Between the item.stock > 0 check and the decrement_stock call, another request can observe the same stock level. Under high concurrency, this allows overselling. The fix uses database-level atomicity:

def purchase_item(user_id: int, item_id: int) -> bool:
    # Atomic check-and-decrement at the database level
    rows_updated = db.execute(
        "UPDATE items SET stock = stock - 1 WHERE id = ? AND stock > 0",
        (item_id,)
    )
    if rows_updated == 0:
        return False
    db.create_order(user_id, item_id)
    return True

SAST tools do not detect race conditions in most configurations. This requires runtime analysis or taint-aware concurrency checkers.

What Actually Catches This: Detection Strategies

1. AI-specific code review checklist

Add a checklist item to your pull request template for code containing AI-generated sections (many developers label these in comments). The checklist should include:

  • Subprocess/shell calls: confirm shell=False or equivalent
  • Cryptographic operations: confirm algorithm selection is current (bcrypt/scrypt/Argon2 for passwords, AES-GCM for symmetric encryption)
  • Error handlers: confirm no stack traces or internal state in response bodies
  • TLS configuration: confirm certificate verification is not disabled
  • Database operations with concurrent access: confirm atomicity

2. Custom SAST rules for AI patterns

Standard SAST rulesets are tuned for generic vulnerable patterns, not the specific forms AI models prefer. Adding custom rules that target the exact patterns above — InsecureSkipVerify: true, innerHTML assignment from non-literal sources, hashlib.sha256 used for password hashing — improves detection significantly.

In Semgrep:

rules:
  - id: go-tls-insecure-skip-verify
    pattern: |
      tls.Config{..., InsecureSkipVerify: true, ...}
    message: InsecureSkipVerify disables certificate validation. Remove before production.
    languages: [go]
    severity: ERROR
  - id: python-weak-password-hash
    patterns:
      - pattern: hashlib.$ALGO($PASSWORD)
      - metavariable-regex:
          metavariable: $ALGO
          regex: (md5|sha1|sha256|sha512)
      - pattern-not-inside: |
          def verify_password(...)
    message: Do not use $ALGO for password hashing. Use bcrypt, scrypt, or Argon2.
    languages: [python]
    severity: ERROR

3. LLM-assisted security review (meta-use)

Ironically, language models are effective at reviewing their own outputs for security properties when prompted correctly. A code review prompt that includes “identify any security hardening omissions, unsafe defaults, or incorrect cryptographic choices in the following code” will catch a meaningful fraction of the issues above — not because the model has special insight but because explicit security framing shifts the generation distribution toward security-aware responses.

This works best as a final-pass review on AI-generated sections before they reach human review, reducing the volume of issues that reach the PR stage.

The core problem with AI code generation and security is not that models are malicious — it is that they optimise for functional correctness and code plausibility, not security properties. Security requires explicit instruction. Every AI-generated function is, by default, an untrusted first draft that needs security review. Building that assumption into your review process is the adjustment AppSec teams need to make.