
Auditing AI-Assisted Apps: Finding Bugs in Code You Didn't Write
The Reality of Auditing AI-Generated Code
This guide shows you how to audit AI-assisted apps and find bugs in code you didn't write, using the same debugging mindset you'd apply to any unfamiliar codebase. Developers are already shipping code generated by Copilot, ChatGPT, and other LLMs into production—code that often reads well but works just well enough to pass a quick review until it doesn't.
I started auditing AI-assisted projects the same way I audit legacy code from a team I just joined: assume nothing, watch runtime behavior, and treat every interaction with the outside world as suspicious. The difference here is that the code wasn't written by someone who might grasp the business logic. It was generated from a prompt and a context window that likely included entirely unrelated examples.
Why Auditing AI-Generated Code Matters
The danger isn't that AI writes worse code in general than humans. It's that AI-generated code often satisfies the prompt's immediate goal without understanding security boundaries, authorization rules, or business invariants that live outside the file. That creates a class of bugs that hide in plain sight. They survive automated tests because those tests rarely assert on what should not happen.
A few things I've seen in real audits:
- An Express route that checked authentication but validated authorization using a client-supplied role.
- A data export function that concatenated user input into a shell command, wrapped in a promise.
- An npm package pulled from an LLM suggestion with one weekly download and an open “update your dependencies” issue from 2020.
These weren't junior mistakes. They were plausible completions that made syntactic sense and were never challenged.
The Auditor's Mindset for Code You Didn't Write
When I audit AI-generated code, I shift my default questions:
- What assumption did the model make that's not in the prompt?
- Where does this code touch a trust boundary?
- What could go wrong if the caller is hostile?
I don't trust variable names. I don't trust const to mean safe. I trace every piece of data back to user input, and I check every interaction with the filesystem, network, database, or child process. The model may have seen similar insecure patterns, and it rarely explains its reasoning before completing the code.
Common Bug Categories in AI-Generated Code
Subtle Logic Errors That Pass Tests
AI-generated code often nails the happy path and ignores edge cases entirely. A function that processes a payment may return true for any non-empty response from the API. A caching layer might cache a 500 error and serve it to the next user. These bugs are subtle because the success case looks fine, and the failure case never gets exercised by the generated tests.
Security Vulnerabilities Waiting to Happen
I keep bumping into this pattern: the model uses a user-controlled string in a context that requires escaping or sanitization, but the surrounding code looks safe. A typical example is a SQL query built with template literals passed to an ORM that allows raw queries.
// ai-generated snippet for a login endpoint
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${hashedPassword}'`;
const result = await db.raw(query);
The developer sees a parameterized-looking call and assumes safety. The raw query is injection-ready.
Hardcoded Secrets and Unsafe Defaults
I've seen API keys, JWT secrets, and test credentials embedded directly in code generated by a model trained on public repos. Unsafe defaults are even more common—debug mode enabled, CORS set to *, or a development-only admin route committed without a guard.
Dependency and Version Pitfalls
Models suggest packages that exist but are unmaintained. They hallucinate version numbers that never existed. I've pulled three different import paths for the same popular library in a single file. Without a lockfile audit, these slip through because the code runs on the developer's machine where the package is already cached.
A Practical Audit Walkthrough
The Suspect Snippet
During a recent code review, I flagged this Express route—but debugging its behavior revealed a deeper authorization flaw.
app.get('/api/reports', authenticateToken, async (req, res) => {
const { userId, month } = req.query;
const filePath = `./reports/${userId}/${month}.csv`;
res.sendFile(filePath, { root: path.join(__dirname, 'private') });
});The route was AI-generated. It checks authentication via middleware. The userId is pulled from the query string. The developer assumed the middleware would validate ownership, but the middleware only verified the JWT signature. The userId in the query could be any value.
Identifying the Hidden Bug
A quick debugging test with a different userId in the query string confirmed the issue:
GET /api/reports?userId=admin&month=january
The server returned the admin user's report. Worse, by changing month to ../../etc/passwd%00, I could test for directory traversal, although the root option limited some traversal.
In-production testing should always use an authorized test account, not a real user's data.
Tracing Impact and Fixing the Issue
Impact: any authenticated user could read reports belonging to another account. The fix wasn't to sanitize the path but to remove the user-supplied userId from the query entirely. The server should derive the user identity from the validated JWT and ignore the client's claim.
const userId = req.user.id; // from token, not query
That one-line change removed the trust assumption the AI had baked in.
Tooling Your Audit Process
I rely on a small stack:
- ESLint with security plugins catches a surprising number of unsafe patterns, including uses of
evalandchild_process. - npm audit / snyk check dependency trees for known vulnerabilities and help flag unmaintained packages.
- Semgrep lets me write custom rules that match patterns I keep seeing in AI output, like raw SQL string concatenation.
- Manual grep for common weak spots:
raw(,exec(,sendFile,createWriteStream.
These tools don't replace manual review, but they shrink the search space.
Building Defensive Practices into AI Workflows
If your team relies heavily on AI-generated code, build guardrails into the workflow:
- Require a short human comment above any AI-generated block that explains why the code is safe.
- Add a dedicated check for injection, authorization, and unsafe defaults in pull request templates.
- Run the generated code through a linter before it reaches a human reviewer.
- Never accept a dependency suggestion without checking its maintenance status and usage.
These steps add friction, but the alternative is trusting code that no one wrote and no one understands.
Conclusion: Trust but Verify
AI-assisted coding is a productivity multiplier. It's also a new attack surface that defers security reasoning to a model with no understanding of your application's threat model. Auditing AI-generated code isn't about blaming the tool. It's about recognizing that the output was completed, not designed. Every line still needs an informed second look, especially where user data crosses a trust boundary.
The bugs I find in AI-assisted apps aren't exotic. They're the same bugs I've been finding in human-written code for years—just with better variable names and less defensive reasoning behind them.


