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=Falseor 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.