Mistakes that ship vulnerabilities

A catalogue of the mistakes AI-assisted engineering ships most often: broken authorisation, insecure defaults, weak crypto, injection, ReDoS, stale APIs, licence contamination, over-broad IAM, swallowed errors and tests that assert the bug. How each looks, why models produce it and how to catch it.

● Must know for interviewsFREELesson·10 min read·Intermediate → Advanced
IDORinsecure defaultsweak cryptoinjectioncode review

01Authorisation that only looks right

✓What you will learn
  • Recognise the recurring vulnerable patterns in generated code
  • Explain why models produce them, so you know where to look
  • Pair each with a review question and an automated control
  • Read vulnerable and fixed versions side by side

Models learn from public code, tutorials and Stack Overflow answers, which optimise for "it works" and are frequently insecure. They also optimise for the request in front of them: ask for an endpoint that returns an alert and you get one that returns any alert. Two patterns dominate.

Trusting generated authorisation checks

The check exists and reads well, but checks the wrong thing: a role from the request body, a isAdmin claim from an unverified token, a middleware that is registered after the route it should protect.

Missing object-level authorisation (IDOR)

IDOR: the most common serious bug in generated APIsts
// Generated: authenticated, not authorised. Any user can read any tenant's alert.
app.get("/api/alerts/:id", requireLogin, async (req, res) => {
  const alert = await db.alert.findUnique({ where: { id: req.params.id } });
  res.json(alert);
});

// Fixed: the query is scoped to the caller; missing and forbidden look the same.
app.get("/api/alerts/:id", requireLogin, async (req, res) => {
  const alert = await db.alert.findFirst({
    where: { id: req.params.id, tenantId: req.session.tenantId },
  });
  if (!alert) return res.status(404).end();
  res.json(alert);
});

Catch it: the review question is "whose object is this, and where is that checked?" for every handler that takes an ID. Automate with an authorisation test matrix (user A requests user B's object, expect 404) in CI, and a lint rule or repository wrapper that refuses unscoped queries on tenant tables.

02Insecure defaults and weak crypto

When a model hits an error it often suggests the change that makes the error go away. Those changes are usually security controls being switched off.

PatternHow it looksWhy AI suggests itCatch it
CORS wildcardorigin: "*" with credentialsFixes the browser error instantlyConfig lint; allow-list per environment
Debug onDEBUG=True, stack traces in responsesTutorials ship with it onEnv-specific config checks in CI
TLS verify offverify=False, rejectUnauthorized: falseFixes self-signed cert errorsSAST rule; fix the trust store instead
ECB or static IVaes-128-ecb, a hardcoded IVShortest working exampleSAST crypto rules; use AEAD helpers
MD5 / SHA-1Hashing passwords or signaturesCommon in older training dataSAST; banned-API list
Math.random tokensReset or session tokens from Math.random()It is the obvious random functionLint rule; require crypto.randomBytes
Weak randomness and crypto, fixedts
// Generated
const resetToken = Math.random().toString(36).slice(2);
const cipher = createCipheriv("aes-256-cbc", key, Buffer.alloc(16, 0)); // static IV, no MAC

// Fixed
const resetToken = randomBytes(32).toString("base64url");               // CSPRNG, 256 bits
const nonce = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, nonce);              // AEAD, unique nonce
!The error was the control working

A certificate error, a CORS rejection or a failing permission check is often the security control doing its job. Fix the cause (trust store, origin allow-list, missing grant) rather than accepting the suggestion that disables the check.

03Injection, ReDoS, stale APIs and licences

SQL built with strings. Models concatenate or template user input into queries because so much example code does. Parameterise always, and let a query builder or ORM do it.

Parameterise, every timets
// Generated
const rows = await db.query("SELECT * FROM alerts WHERE host = '" + host + "'");

// Fixed
const rows = await db.query("SELECT * FROM alerts WHERE host = $1", [host]);

Regex DoS. Nested quantifiers like ^(\w+\s?)*$ backtrack exponentially on a crafted string, and a single request can pin a CPU. Log parsers and input validators generated by a model are a frequent source. Catch it with ReDoS linters, length limits before matching, and linear-time engines (RE2) for untrusted input.

Outdated APIs. Training data lags. Expect deprecated functions (crypto.createCipher), old library versions with known CVEs, and auth flows that are no longer recommended (OAuth implicit flow). Dependency scanning and a pinned, current lockfile catch most of it.

Licence contamination. A model can reproduce substantial chunks of copyleft code. Shipping it inside a commercial product can create obligations you did not intend. Use tools with code-reference filtering, run licence scanning, and treat large verbatim-looking blocks with suspicion.

04Over-broad IAM, swallowed errors and tests that assert the bug

Over-broad IAM generated for convenience. Ask for a policy that lets a Lambda read a bucket and you may get s3:* on *. It works, so it ships.

Least privilege, not least effortjson
// Generated
{ "Effect": "Allow", "Action": "s3:*", "Resource": "*" }

// Fixed: one action, one bucket prefix
{ "Effect": "Allow", "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::cyart-evidence/tenant-*/reports/*" }

Swallowed errors. try { ... } catch { } or catch (e) { return true } hides failures and, in a security check, fails open. Lint for empty catches and review every catch in an auth path.

Tests that assert the bug. Generate tests from the code and they faithfully lock in its behaviour. If the handler returns another tenant's alert, the generated test asserts status 200. Tests must come from the spec, and must include the negative and cross-tenant cases.

MistakeReview questionAutomated control
Trusted authz checkWhere does each authz input come from?Authz test matrix, DAST
IDORWhose object is this and where is that checked?Cross-tenant tests, scoped repository
Insecure defaultsWhat did this change switch off?Config lint, IaC scanning
Weak crypto / randomnessIs this a vetted high-level API?SAST crypto rules, banned APIs
String-built SQLIs any input concatenated into a query?SAST injection rules
ReDoSCan untrusted input reach this regex?ReDoS lint, RE2, input length caps
Outdated APIIs this the current recommended API?Dependency and deprecation scanning
Licence contaminationDoes this look copied verbatim?Licence scanning, reference filters
Over-broad IAMWhat is the smallest action and resource?IaC policy checks, access analyser
Swallowed errorsWhat happens on failure?Lint for empty or permissive catch
Tests assert the bugWas this test written from the spec?Mutation testing, negative-case coverage
“How to say it in the room

"Generated code fails in predictable ways: it authenticates but does not authorise, it switches off the control that raised an error, it reaches for the shortest crypto example, and its tests assert whatever the code does. I review with a fixed set of questions for those patterns and back each one with a CI control, so catching them does not depend on a tired reviewer."

Test yourself

A generated test for `GET /api/alerts/:id` logs in as tenant A, requests an alert owned by tenant B and asserts status 200 with the alert body. What does this tell you?

Q1Why do models suggest `verify=False` so often?+

Because it is the most common answer to certificate errors in public code, and it makes the error disappear. The right fix is to add the internal CA to the trust store.

Q2How do you find generated code with a known-bad pattern after the fact?+

Provenance tags in PRs narrow the search, and SAST rules written for the pattern find it across the codebase regardless of who wrote it.

Q3What is mutation testing and why does it help here?+

It changes the code in small ways (flip a comparison, delete a check) and expects tests to fail. Tests that still pass after the authz check is deleted are not testing authorisation.

Finished this one?
Tick it and it shows as done on the roadmap and the course.