
Testing AI-Generated Code for XSS, CSRF, and Broken Authorization
Why AI-generated code needs a security review
AI-generated code usually fails in familiar ways, which is exactly why it slips through review. It moves data from one layer to another without stopping to ask whether that data is trusted. In a browser app, that often means user input ends up in the DOM, a state-changing request ships without a real anti-CSRF check, or the UI hides an action that the server still accepts.
I review generated code the same way I would review code from a rushed junior engineer: useful, but not trusted until I trace the data flow.
The bigger risk is not that the model invents some new exploit. It is that it recreates old web mistakes very quickly.
The three failure modes that show up most often
XSS from unsafe rendering and HTML escaping mistakes
This usually appears when generated code uses innerHTML, a dangerously formatted template, or string concatenation with user-controlled values. The model often assumes “escape somewhere later” will be enough. It usually is not.
My quick check is simple: if user content becomes HTML, I look for the exact sink and the exact escaping rule. If either one is vague, I keep digging.
CSRF from missing origin checks and weak state-changing routes
AI-generated backend code often produces tidy-looking endpoints that accept POST, PATCH, or DELETE without any real CSRF defense. It may verify that the user is logged in, which is not the same thing as proving the request came from your app.
If the route changes server state, test whether it accepts a forged request from another origin, or at least whether it validates a token, origin, or same-site cookie strategy consistently.
Broken authorization from trusting UI state or client-side checks
This is the one I see most often in real apps. The frontend hides a button, but the API still accepts the action. Or the code checks isAdmin in the client, which is only a display flag, not a security boundary.
The impact is usually straightforward: a low-privilege user can read, update, or delete something they should never touch.
A practical test workflow in JavaScript
Build a minimal repro and trace the data flow
Start with the smallest path from input to sink. In practice, I reduce the test case to one component, one request, and one server handler.
const input = new URLSearchParams(location.search).get("q");
document.querySelector("#result").innerHTML = input;
That tiny example is enough to ask the right question: where is input validated, escaped, and rendered?
For authorization, I do the same thing with network calls. If a button triggers fetch("/api/account", { method: "POST" }), I check whether the backend verifies ownership or role, not just whether the UI showed the button.
Inspect rendered output, requests, and server-side checks
Use browser devtools and a proxy, then compare what the UI claims with what the server actually enforces.
| Layer | What to inspect | What usually goes wrong |
|---|---|---|
| DOM | innerHTML, dangerouslySetInnerHTML, template rendering | Unescaped user content reaches HTML |
| Network | request method, headers, cookies, origin | State change has no CSRF defense |
| API | ownership checks, role checks, object lookup | Client-side restrictions are trusted |
I also check whether the server rejects requests when I remove UI-only fields like isAdmin, canEdit, or role. If the request still works, the client was pretending to enforce policy.
Use safe payloads to confirm impact without causing damage
You do not need destructive payloads to prove a bug.
For XSS, a harmless marker is enough:
const payload = `<img src=x onerror="console.log('xss-test')">`;
For CSRF, I test whether a request can be triggered from a cross-site context and whether the server rejects it cleanly. For authorization, I use a second test account with lower privileges and try to access a known protected object.
The goal is evidence, not chaos.
What good fixes look like
Fix XSS at the sink, not with ad hoc filters
The safe fix is to keep untrusted data out of HTML sinks unless you have a proven sanitizer and a strict allowlist. Prefer text nodes, framework escaping, or templating that encodes by default.
If the code must render rich text, sanitize on the server or with a trusted library before output. Do not bolt on string replacement and call it done.
Add CSRF defenses where state changes happen
Protect the route, not just the page. For cookie-based sessions, use anti-CSRF tokens, same-site cookie settings, and origin or referer checks where appropriate. Then verify that every state-changing endpoint enforces the same policy.
A login page that is safe does not protect a delete route that forgot the token.
Enforce authorization on the backend for every sensitive action
Never rely on hidden buttons, disabled fields, or client flags. The server should check ownership, role, and scope for every protected object before it reads or mutates it.
If a request includes userId, projectId, or accountId, assume the client can tamper with it. Re-derive trust from the authenticated identity on the server.
Common patterns AI models get wrong
- using
innerHTMLbecause it is convenient - checking permissions only in React state
- assuming
POSTis automatically safe from CSRF - forgetting that cookies are sent by the browser, not by “the app”
- using request parameters as proof of identity
- sanitizing inputs in one place and rendering them unsafely in another
The pattern is not “the model is insecure.” The pattern is that it optimizes for a working demo, not a secure boundary.
Checklist for reviewing generated code before it ships
- Trace user input from source to sink.
- Confirm every HTML sink escapes or sanitizes correctly.
- Verify every state-changing route has CSRF protection.
- Test that authorization is enforced on the server, not the UI.
- Try a second account with fewer privileges.
- Remove client-only fields and see whether the request still succeeds.
- Use safe proof payloads and capture the exact impact.
- Fix the backend first, then clean up the frontend.
Conclusion
AI-generated code is fast, but it is not disciplined about trust boundaries. If you review it like any other untrusted implementation, you will catch the same classes of bugs that keep showing up in production: XSS, CSRF, and broken authorization. The useful habit is simple: follow the data, test the request, and never assume the UI is the security layer.


