AI Coding

Security Mistakes in Generated Auth and SQL

Auth code and SQL are the two places where AI-generated code fails most expensively. A wrong button color is a nuisance. A wrong WHERE clause is a data breach. A wrong session check is an open door with a sign taped to it. The uncomfortable part: both kinds of code look fine.

Generated auth flows arrive wearing the costume of correctness. There is a hashing call, a token, an expiry field, a redirect. The syntax is clean, the variable names are reasonable, and the tests pass because the tests were generated by the same model that wrote the code, which means they test the shape of the thing rather than the safety of it.

The failures cluster. They are not random. Models reproduce the same handful of security mistakes across projects, frameworks, and languages, because those mistakes are present in the training data in enormous quantities and because the mistakes are invisible to the casual reading that generated code usually gets.

Password hashing that uses the wrong tool correctly

Ask a model for password storage and it will often reach for SHA-256 or MD5, wrapped in a salt, iterated once. The code is valid. The output is a hex string of the right length. Everything about it reads as competent.

This is a category error: a general-purpose hash function is built to be fast, and fast is the exact property a password hash must not have. Modern password hashing (bcrypt, scrypt, Argon2) has a work factor, a cost parameter, a deliberate slowness that can be tuned upward as hardware improves. A single SHA-256 pass over a salted password can be tried billions of times per second on commodity GPUs.

The tell is the loop. Generated code that hashes once and stores the result is wrong in a way no amount of code review by someone unfamiliar with the distinction will catch. The fix is boring and well documented: bcrypt with a cost of at least 10, Argon2id with sensible memory and iteration settings, and a library that manages the salt for you so nobody reinvents the format.

Tokens compared with the equal sign

A password reset token, an API key, a signed session value: all of these get checked against a stored value. Generated code reaches for the obvious comparison, a simple equality check, and moves on.

That comparison leaks timing. Byte-by-byte equality short-circuits on the first mismatch, so an attacker who can measure response times with enough precision can recover a valid token one character at a time. The attack is fiddly over a network and trivial in some contexts, and the defense is a constant-time comparison function that the standard library almost certainly provides.

Nobody writes the constant-time version from memory. That is precisely why it needs to be a checklist item rather than a matter of inspiration.

JWT validation with the signature check removed

JSON Web Tokens invite a specific mistake: decoding the payload without verifying the signature. The decoded payload contains the user ID and the role, and the application trusts it. An attacker can then mint their own token with any claims they like, and the server reads it back with a straight face.

Generated code sometimes calls a decode function that does not verify by default. Libraries differ here, and the difference is not obvious from the call site. Some decode helpers split the token, base64-decode the middle segment, and return the JSON, signature untouched and unchecked. The code compiles, the happy path works, and the auth is decorative.

A close relative: accepting the algorithm from the token header. If the server honors whatever alg the token declares, an attacker can switch to none or to a symmetric algorithm where an asymmetric one was expected. Pin the algorithm server-side and verify the signature with a function whose name contains the word verify.

Session state that never expires on logout

Logout in generated code often means clearing the cookie on the client. The server-side session or token remains valid until its natural expiry, which might be hours or days. A stolen token continues to work after the user believes they have logged out.

The same pattern shows up in password changes: the old sessions are not invalidated, so a compromised account stays compromised after the password is reset. This is the kind of bug that shows up in incident reports months later, when the timeline does not add up.

Session invalidation requires server-side state: a token version number, a session store, a revocation list. Generated code tends toward stateless designs because they are simpler to write and demonstrate, and stateless auth has no natural place to record "this credential is dead now."

Reset flows that trust the email parameter

Password reset is where generated auth code gets creative in the wrong direction. A common shape: the reset endpoint accepts an email address, looks up the user, and returns a token in the response body. That hands an attacker an account takeover primitive. The corrected version emails the token to the address on file, and the endpoint returns a generic success message regardless of whether the account exists, to avoid user enumeration.

Generated resets also skip expiry, skip single-use enforcement, or generate tokens with a weak random source. The fix for the last one is a cryptographic random generator, not the language's default random function, which is seeded predictably and is fine for games and useless for secrets.

String-concatenated SQL with a comment that says it is parameterized

SQL injection is old enough to have a retirement party, and generated code still produces it. The mechanism is consistent: a query string is assembled with concatenation or an f-string, and user input lands inside the quotes. The model often writes a comment claiming the query is parameterized. It is not.

The subtler version uses the right library but the wrong method. An ORM offers a raw query function and a parameterized one, and the generated code picks the raw one because the parameterized one had a slightly awkward signature. The vulnerability is one function call away from being absent, which makes it easy to miss in review.

Search filters are the usual entry point. A sort column taken from a query parameter, interpolated into an ORDER BY clause, cannot be parameterized in most databases, so generated code interpolates it and calls it a day. The correct handling is an allowlist: map the incoming value to a known set of column names and reject everything else. The same applies to table names, direction keywords, and anything else that is syntax rather than data.

Authorization checked at the route and nowhere else

Generated code tends to put an auth check on the route and treat that as the whole job. The route requires a logged-in user, so the handler assumes the user is allowed to touch the resource identified in the URL. Nothing checks that the resource belongs to that user.

This is the classic insecure direct object reference, and it is the most common serious flaw in generated CRUD applications. The endpoint /invoices/41 returns invoice 41 to anyone with a valid session. The fix is an ownership check in the query itself: the WHERE clause includes the user ID, so a request for someone else's invoice returns zero rows.

Row-level authorization is a design decision, and generated code makes it implicitly by omitting it. The route-level check feels like security because it is visible and it rejects anonymous requests. The test that matters is a second user with a valid session asking for the first user's data.

The error message that describes the schema

Generated SQL error handling often passes the database error straight to the client. That message names columns, tables, constraints, and sometimes the exact query. An attacker probing for injection points gets a free schema map with every malformed request.

The same pattern appears in auth: a login failure that says "no such user" versus "wrong password" hands over a user enumeration oracle. The fix is a single generic message for both cases, logged differently on the server where the detail is useful.

Migrations that add a column with no constraint

Generated migration files create tables with nullable columns and no unique constraints on the fields that obviously need them. An email column with no unique index means duplicate accounts are possible, and the login query that fetches "the user with this email" returns whichever row the database feels like returning.

Foreign keys are frequently omitted too, so orphaned rows accumulate and the application's assumption of referential integrity quietly rots. These are not injection bugs, but they are security bugs in slow motion: duplicate identities, missing audit trails, records that belong to nobody.

What actually catches these before production

Reading generated code carefully does not work, because the failures are omissions and category errors rather than visible mistakes. The practices that do work are mechanical and unglamorous.

Run a static analysis tool configured for security rules. Semgrep, CodeQL, and the language-specific linters have rules for raw SQL construction, weak hashing, and missing authorization, and they catch these patterns without understanding the intent of the code. Run a dependency scanner, because generated code pulls in packages with known vulnerabilities at a rate that surprises people.

Write the negative tests by hand. The test suite should include a request for another user's resource, a login with a wrong password that must produce the same message as a nonexistent user, a request with a tampered JWT, and a query parameter containing a single quote. Generated tests will not include these, because the model writes tests that confirm the code does what it appears to do.

Then read the auth and data-access layers line by line, specifically hunting for the patterns above: a hash call without a cost parameter, an equality comparison on a token, a decode without a verify, a WHERE clause built by string formatting, a route handler that never mentions the user ID. The list is short and it is finite, which is the one piece of good news here.

The model will keep producing this code, and it will keep looking correct. The only reliable defense is a reviewer who knows the exact shapes of the failures, a checklist that names them, and the discipline to run it on every generated endpoint before the first real user signs in.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button