The Problem
You walked the AI-generated diff through the OWASP Top 10 and it came back clean. No SQL injection, auth checks in place, secrets in env vars, dependencies patched. You ship. A week later, the Python service deserializes a malicious pickle from an internal queue and someone owns your worker fleet. OWASP didn't fail; it just doesn't have a category for "your language's specific footgun."
The core issue: each language has a private collection of security pitfalls that don't map cleanly to OWASP categories. Python's pickle. JavaScript's __proto__. Go's unsynchronized maps. Rust's unwrap(). Java's readObject(). AI reproduces these patterns because they're idiomatic and appear constantly in training data — "typical" code in each language is also "typical-vulnerable" code in each language.
The Core Insight
OWASP covers what attackers do to web apps. Language footguns cover what your runtime lets attackers do once they're inside. You need both passes. The OWASP checklist (covered in the sibling guide) handles the generic categories. This guide handles what OWASP misses.
Each language has a short, memorable list of footguns — usually 3 to 5 — and almost every AI-introduced vulnerability in that language is one of them. Memorize your stack's list, ripgrep for the red-flag tokens, and you'll catch 80% of language-specific issues in under a minute.
Think of it as code smells for security: each language has patterns that scream "vulnerable" the moment they appear in a diff. The rest of this guide is those lists.
Python: The Top 5 AI Vulnerabilities
1. Unsafe Deserialization (pickle)
RED FLAG: import pickle
If you see import pickle in AI-generated code, treat it as a critical red flag. pickle.load() deserializes arbitrary Python code — it is a remote-code-execution primitive disguised as a file format. AI reaches for it because it's terse and ubiquitous in tutorials. Replace with json, yaml.safe_load(), or msgpack on sight.
Why AI does this: pickle is common in Python tutorials. AI copies the pattern.
# Bad: AI loves to generate this
import pickle
def load_data(filename):
with open(filename, 'rb') as f:
return pickle.load(f) # Arbitrary code execution!
# Attacker can craft malicious pickle to run commands
# Good: Use safe serialization
import json
def load_data(filename):
with open(filename, 'r') as f:
return json.load(f)
Fix: Replace pickle with json, yaml.safe_load(), or msgpack.
2. YAML Deserialization (yaml.load without SafeLoader)
Why AI does this: tutorials use yaml.load(f) because it's one fewer argument. Same RCE class as pickle — PyYAML's default loader instantiates arbitrary Python objects from !!python/object/apply tags.
# Bad: arbitrary object construction
import yaml
config = yaml.load(open('config.yml')) # RCE primitive
# Good: explicit safe loader
config = yaml.safe_load(open('config.yml'))
3. Command Injection via shell=True
Why AI does this: os.system() and subprocess.run(..., shell=True) are the shortest code path. Both invoke /bin/sh -c, which interprets shell metacharacters from user input.
# Bad: shell parses the entire string
import os
filename = request.form['filename']
os.system(f"cat {filename}") # Attacker: file.txt; rm -rf /
subprocess.run(f"cat {filename}", shell=True) # Same problem
# Good: list args, no shell
import subprocess
subprocess.run(['cat', filename], check=True, capture_output=True)
4. Path Traversal
Why AI does this: Trusts user input for file paths.
# Bad: Direct file access
file_path = request.args.get('file')
with open(f'/uploads/{file_path}', 'r') as f:
return f.read()
# Attacker sends: file=../../etc/passwd
# Good: Validate and sanitize
import os
from pathlib import Path
UPLOAD_DIR = '/uploads'
file_path = request.args.get('file')
safe_path = (Path(UPLOAD_DIR) / file_path).resolve()
if not str(safe_path).startswith(UPLOAD_DIR):
raise ValueError("Invalid file path")
with open(safe_path, 'r') as f:
return f.read()
5. Timing Attacks in Authentication
Why AI does this: Uses standard equality checks.
# Bad: Early return leaks timing
def check_token(provided, expected):
if provided == expected: # Short-circuits on first mismatch
return True
return False
# Good: Constant-time comparison
import hmac
def check_token(provided, expected):
return hmac.compare_digest(provided, expected)
Python-Specific Scan
Search codebase for: pickle.load, eval(, exec(, os.system(, f"SELECT, == (in auth code)
JavaScript/Node.js: The Top 5 AI Vulnerabilities
1. Prototype Pollution
Why AI does this: Dynamic object manipulation is common in JS.
// Bad: User can pollute Object prototype
function merge(target, source) {
for (let key in source) {
target[key] = source[key];
}
}
// Attacker sends: {"__proto__": {"isAdmin": true}}
// Good: Use Object.assign with safe guards
function merge(target, source) {
const safeKeys = Object.keys(source).filter(
key => !['__proto__', 'constructor', 'prototype'].includes(key)
);
for (let key of safeKeys) {
target[key] = source[key];
}
}
2. ReDoS (Regular Expression Denial of Service)
Why AI does this: Generates complex regex without considering performance.
// Bad: Catastrophic backtracking
const emailRegex = /^([a-zA-Z0-9_\.\-])+@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
// Attacker sends: "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
// Causes exponential regex execution time
// Good: Simpler regex or dedicated library
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// Or use: npm install email-validator
3. XSS via innerHTML
Why AI does this: Shortest code for DOM manipulation.
// Bad: Direct HTML injection
element.innerHTML = userInput;
// Attacker sends: <img src=x onerror=alert('XSS')>
// Good: Use textContent or sanitize
element.textContent = userInput;
// If HTML needed:
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
4. JWT Secret Hardcoding
Why AI does this: Examples use hardcoded secrets for simplicity.
// Bad: Hardcoded secret
const token = jwt.sign({ userId }, 'secret123');
// Good: Environment variable
const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: '24h'
});
5. NoSQL Injection (MongoDB)
Why AI does this: Direct object queries seem safe but aren't.
// Bad: Direct query object
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
// Attacker sends: {"username": {"$ne": null}, "password": {"$ne": null}}
});
// Good: Explicit validation
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Ensure strings, not objects
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await User.findOne({ username });
const valid = await bcrypt.compare(password, user.passwordHash);
// ... rest of auth
});
Go: The Top 5 AI Vulnerabilities
1. Ignored error Return Values
Why AI does this: Go's val, _ := f() idiom is everywhere in tutorials. The model silently drops the error and the caller proceeds with zero-value val, which often means "treat unauthenticated user as authenticated."
// Bad: error dropped, claims becomes zero value
claims, _ := jwt.ParseWithClaims(token, &Claims{}, keyFn)
if claims.UserID == "admin" { ... } // skipped verification
// Good: handle every error from a security primitive
claims, err := jwt.ParseWithClaims(token, &Claims{}, keyFn)
if err != nil { return ErrUnauthorized }
2. Race Conditions (Concurrent Map Access)
Why AI does this: Generates concurrent code without sync primitives.
// Bad: Unsynchronized map writes
var cache = make(map[string]int)
go func() {
cache["key"] = 1 // Race condition!
}()
go func() {
cache["key"] = 2 // Race condition!
}()
// Good: Use sync.Map or mutex
import "sync"
var (
cache = make(map[string]int)
mu sync.RWMutex
)
go func() {
mu.Lock()
cache["key"] = 1
mu.Unlock()
}()
3. Path Traversal in filepath.Join
// Bad: filepath.Join doesn't prevent traversal
file := filepath.Join("/uploads", userPath) // Can escape with ../
// Good: Validate result
file := filepath.Join("/uploads", userPath)
if !strings.HasPrefix(file, "/uploads") {
return errors.New("invalid path")
}
4. Unbounded Goroutines
Why AI does this: Loves spawning goroutines without limits.
// Bad: Spawn goroutine per request
for _, item := range items {
go processItem(item) // Unbounded concurrency
}
// Good: Worker pool
semaphore := make(chan struct{}, 10) // Max 10 concurrent
for _, item := range items {
semaphore <- struct{}{}
go func(item Item) {
defer func() { <-semaphore }()
processItem(item)
}(item)
}
5. Unsafe Type Assertions
// Bad: Panic on wrong type
value := data.(string)
// Good: Check assertion
value, ok := data.(string)
if !ok {
return errors.New("invalid type")
}
Rust: The Top 3 AI Vulnerabilities
Note: Rust's compiler catches many vulnerabilities, but AI can still introduce logic errors.
1. Unsafe Blocks (use-after-free, buffer overrun)
// Bad: AI adds unsafe when it doesn't know the safe way
unsafe {
let data = *raw_ptr; // Could be freed already
}
// Good: Avoid unsafe unless absolutely necessary
// Use safe abstractions instead
2. Integer Overflow in Release Mode
// Bad: Silent overflow in release builds
let result = a + b; // Overflows silently
// Good: Checked arithmetic
let result = a.checked_add(b).ok_or("overflow")?;
3. Denial of Service via Unbounded Resources
// Bad: Read entire file into memory
let contents = fs::read_to_string(user_file)?;
// Good: Stream large files
use std::io::{BufRead, BufReader};
let file = File::open(user_file)?;
let reader = BufReader::new(file);
for line in reader.lines().take(1000) { // Limit lines
// Process line
}
4. .unwrap() / .expect() on Untrusted Input
Why AI does this: ? is correct but .unwrap() is shorter and shows up in every "hello world" tutorial. On user-controlled input it turns a parse failure into a process-killing panic — a one-line DoS primitive.
// Bad: any malformed input crashes the worker
let id: u64 = req.param("id").unwrap().parse().unwrap();
// Good: propagate the error
let id: u64 = req.param("id")
.ok_or(Error::MissingId)?
.parse()
.map_err(|_| Error::BadId)?;
Java: The Top 4 AI Vulnerabilities
1. ObjectInputStream.readObject()
RED FLAG: readObject() on untrusted input
Java's native serialization deserializes arbitrary classes from a byte stream. With the right gadget chain on the classpath (Commons Collections, Spring, etc.) this is a remote-code-execution primitive. Same risk class as Python's pickle. AI reaches for it because every Java tutorial uses it. Replace with Jackson, Gson, or protobuf on sight.
// Bad: arbitrary gadget chain executes during deserialization
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Order order = (Order) in.readObject();
// Good: explicit, type-safe deserialization
ObjectMapper mapper = new ObjectMapper();
Order order = mapper.readValue(socket.getInputStream(), Order.class);
2. XXE in XML Parsers (default config)
Why AI does this: DocumentBuilderFactory.newInstance() returns a parser with external entities enabled by default. AI writes one line; the parser fetches file:///etc/passwd from a crafted XML body.
// Good: disable XXE on every factory
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
3. SnakeYAML Default Loader
Same risk as Python's yaml.load: new Yaml().load(input) instantiates arbitrary classes. Use new Yaml(new SafeConstructor()).
4. Reflection from User Input
Why AI does this: Generic "do thing by name" patterns. Letting users pick the class name turns reflection into eval.
// Bad: attacker picks the class
Class<?> cls = Class.forName(req.getParameter("type"));
Object instance = cls.getDeclaredConstructor().newInstance();
// Good: allowlist
Map<String, Class<?>> allowed = Map.of(
"user", User.class, "order", Order.class
);
Class<?> cls = allowed.get(req.getParameter("type"));
if (cls == null) throw new BadRequestException();
Quick Reference
Language-Specific Security Scans:
Python: ripgrep for:
pickle.load,yaml.load((without SafeLoader),eval(,exec(→ RCEshell=True,os.system(→ command injection==in token/HMAC comparisons → timing attack (usehmac.compare_digest)
JavaScript/Node: ripgrep for:
innerHTML,eval(→ XSS & RCE__proto__,Object.assign({}, userInput)in merges → prototype pollution- Complex backtracking regex on user input → ReDoS
- MongoDB
findOne({ password })without type check → NoSQL operator injection
Go: ripgrep for:
, _ :=on a security-critical call (jwt parse, password compare) → dropped error- Map writes inside
go func()withoutsync.Mutex/sync.Map→ race filepath.Joinwith user input + noHasPrefixcheck → path traversal- Unbounded
gospawn insidefor→ goroutine flood DoS
Rust: ripgrep for:
.unwrap()/.expect(on parsed user input → panic DoSunsafe {blocks → review every use-after-free / OOB path- Raw
a + bin release builds with user-controlled operands → silent overflow
Java: ripgrep for:
ObjectInputStream,readObject(→ deserialization RCEDocumentBuilderFactory.newInstance()without XXE hardening → XXEnew Yaml(withoutSafeConstructor→ SnakeYAML RCEClass.forName(with user input → reflection abuse
Language-Specific AI Audit Prompts:
# Python
"Audit for pickle/yaml/eval RCE, shell=True command injection, and == in auth comparisons"
# JavaScript
"Audit for prototype pollution, innerHTML XSS, ReDoS, and NoSQL operator injection"
# Go
"Audit for dropped errors on security calls, unsynchronized map writes, and goroutine floods"
# Rust
"Audit for unwrap/expect on untrusted input, unsafe blocks, and integer overflow"
# Java
"Audit for readObject deserialization, XXE in XML parsers, SnakeYAML default loader, and reflection from user input"
Key Takeaways
What To Remember
- OWASP isn't enough. The Top 10 covers what attackers do to web apps. Language footguns cover what your runtime lets attackers do once they have a foothold. Run both passes.
- Each language has ~5 usual suspects. Memorize your stack's list and grep for the red-flag tokens — you'll catch 80% of language-specific vulnerabilities in under a minute.
- Deserialization is the universal footgun. Python
pickle, JavareadObject, SnakeYAML default loader, PyYAMLyaml.load— same RCE class wearing different syntax. Treat every one as critical. - Idiomatic and "typical" code is often vulnerable code. AI prefers terse, common patterns. The shortest path in each language is usually the unsafe one.
- Silent failures and dropped errors are exploits in disguise. Go's
val, _ := ..., Rust's.unwrap(), JavaScript's loose-equals comparisons. The footgun is in what the language lets you skip.
Related Guides
This guide pairs with the other two parts of the security trio:
- The OWASP Top 10 Checklist for AI-Generated Code — the systematic OWASP pass that covers the generic web-app categories this guide deliberately skips.
- Shift-Left Security with AI Assistance — how to wire these language-specific scans into your editor and CI so the footguns never reach review.