AI Security Audit Checklist: 15 Vulnerabilities Claude Found in Production Code An engineer used Claude to audit production code and identified 15 vulnerabilities from the OWASP Top 10, compressing a typical 2-3 week audit into hours. The most common findings include SQL injection and NoSQL injection, with detailed examples of vulnerable code, attack vectors, and fixes provided. The audit process involves three passes: broad scan, deep analysis, and manual verification. Most web applications contain at least one vulnerability from the OWASP Top 10. A typical security audit takes 2-3 weeks and costs upward of $10,000. An LLM can compress the initial audit down to a few hours because it scans code for patterns rather than specific CVEs. Below are 15 vulnerabilities found while auditing production code with Claude. Each includes the vulnerable code, the fixed version, and a prompt to reproduce the finding. Classification follows OWASP Top 10 2021 . Order reflects frequency of occurrence: most common first. The audit consists of three passes. First, a broad scan: the LLM receives the entire project and looks for vulnerability patterns. Second, deep analysis: each identified pattern is verified in context middleware, ORM, framework . Third, verification: manual review of every finding, because LLMs produce false positives. Prompt for the broad scan: Perform a security audit of this code. For each finding, include: 1. CWE ID and name 2. OWASP Top 10 category 3. Severity Critical/High/Medium/Low 4. The vulnerable code snippet 5. Attack vector -- exactly how an attacker would exploit this 6. Fixed code Ignore stylistic comments. Focus on security only. Start with injection attacks, then broken access control, then the rest. This prompt works because it defines the output structure and prioritizes categories. Without explicit instructions, the LLM mixes critical vulnerabilities with remarks about email validation. More on structured AI code review: AI Code Review Checklist https://dev.to/blog/claude-concilium-multi-agent-code-review/ . The most common finding. Shows up even in projects using an ORM, because developers switch to raw queries for complex filters. Vulnerable code: js // API endpoint for user search app.get '/api/users', async req, res = { const { search, sortBy } = req.query; const query = SELECT id, name, email FROM users WHERE name LIKE '%${search}%' ORDER BY ${sortBy} ; const result = await db.query query ; res.json result.rows ; } ; Attack vector: GET /api/users?search='; DROP TABLE users; --&sortBy=id Fixed code: js app.get '/api/users', async req, res = { const { search, sortBy } = req.query; const allowedSortColumns = 'id', 'name', 'email', 'created at' ; const sanitizedSort = allowedSortColumns.includes sortBy ? sortBy : 'id'; const query = SELECT id, name, email FROM users WHERE name LIKE $1 ORDER BY ${sanitizedSort} ; const result = await db.query query, %${search}% ; res.json result.rows ; } ; Parameterized query for values, whitelist for identifiers column names . ORDER BY cannot be parameterized in most drivers, so the whitelist is mandatory. // Vulnerable: req.body passed directly into the query app.post '/api/login', async req, res = { const user = await db.collection 'users' .findOne { username: req.body.username, password: req.body.password, } ; if user return res.json { token: generateToken user } ; res.status 401 .json { error: 'Invalid credentials' } ; } ; Attack vector: POST /api/login with body {"username": "admin", "password": {"$ne": ""}} . The $ne not equal operator turns the password check into "password is not equal to empty string" -- true for any user. js // Fixed: explicit string casting app.post '/api/login', async req, res = { const username = String req.body.username ; const password = String req.body.password ; const user = await db.collection 'users' .findOne { username } ; if user || await bcrypt.compare password, user.passwordHash { return res.status 401 .json { error: 'Invalid credentials' } ; } res.json { token: generateToken user } ; } ; Two fixes: String blocks MongoDB operators, and bcrypt.compare replaces plaintext password comparison. // Vulnerable: user input in a shell command app.post '/api/convert', async req, res = { const { filename } = req.body; exec convert uploads/${filename} -resize 200x200 thumbnails/${filename} , err, stdout = { if err return res.status 500 .json { error: 'Conversion failed' } ; res.json { status: 'ok' } ; } ; } ; Attack vector: filename: "image.png; rm -rf /" js // Fixed: execFile instead of exec, filename validation import { execFile } from 'child process'; app.post '/api/convert', async req, res = { const { filename } = req.body; if /^ a-zA-Z0-9 - +\. png|jpg|webp $/.test filename { return res.status 400 .json { error: 'Invalid filename' } ; } execFile 'convert', uploads/${filename} , '-resize', '200x200', thumbnails/${filename} , err = { if err return res.status 500 .json { error: 'Conversion failed' } ; res.json { status: 'ok' } ; } ; } ; execFile does not spawn a shell, so ; rm -rf / is not interpreted as a separate command. The regex restricts allowable characters. // Vulnerable: any authenticated user can view any order app.get '/api/orders/:id', authMiddleware, async req, res = { const order = await db.query 'SELECT FROM orders WHERE id = $1', req.params.id ; res.json order.rows 0 ; } ; Attack vector: ID enumeration -- GET /api/orders/1 , /api/orders/2 , /api/orders/3 ... // Fixed: ownership check app.get '/api/orders/:id', authMiddleware, async req, res = { const order = await db.query 'SELECT FROM orders WHERE id = $1 AND user id = $2', req.params.id, req.user.id ; if order.rows 0 return res.status 404 .json { error: 'Not found' } ; res.json order.rows 0 ; } ; AND user id = $2 added. If a role legitimately allows access to others' orders admin, support , the check happens through RBAC middleware rather than through the absence of a condition. Vulnerable: user controls the file path @app.route '/api/files/