---
name: review-security
description: >-
Audit AI-generated front-end code for security vulnerabilities before deploy.
Use after completing any feature that handles user input, renders dynamic
content, stores data, authenticates users, or communicates with APIs.
---
# Security Review
AI coding tools produce functional code but routinely introduce security
vulnerabilities. They use `innerHTML` when `textContent` works, skip input
sanitization, hardcode secrets, and generate permissive CORS configurations.
This skill catches those issues before they ship.
This is a review skill. It doesn't teach secure coding from scratch — it
teaches the agent to audit code it already wrote and flag specific
vulnerability patterns.
## Cross-Site Scripting (XSS)
### Audit every dynamic content insertion
XSS is the most common vulnerability in AI-generated front-end code. AI
tools default to `innerHTML` because it's the simplest way to render HTML,
but it executes any script embedded in the content.
**Check every instance of:**
```javascript
// Dangerous — user content executes as HTML
element.innerHTML = userContent;
element.outerHTML = userContent;
document.write(userContent);
// Safe alternatives
element.textContent = userContent; // Plain text only
element.setAttribute('href', url); // Single attribute
```
If you genuinely need to render HTML from user content, sanitize it:
```javascript
function sanitizeHTML(html) {
const template = document.createElement('template');
template.innerHTML = html;
template.content.querySelectorAll('script, iframe, object, embed, form')
.forEach(el => el.remove());
template.content.querySelectorAll('*').forEach(el => {
for (const attr of el.attributes) {
if (attr.name.startsWith('on') || attr.value.startsWith('javascript:')) {
el.removeAttribute(attr.name);
}
}
});
return template.innerHTML;
}
```
### Check template literals in HTML context
```javascript
// Dangerous — interpolation in HTML context
container.innerHTML = `<div class="${className}">${userInput}</div>`;
// Safe — build with DOM API
const div = document.createElement('div');
div.className = className;
div.textContent = userInput;
container.appendChild(div);
```
### React/JSX specific
```jsx
// Dangerous — the name says it all
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// Safe
<div>{userContent}</div>
```
Every use of `dangerouslySetInnerHTML` needs justification and a sanitization
step. If it exists without sanitization, it's a vulnerability.
### Never use `eval()` or `new Function()`
`eval()` and `new Function()` execute arbitrary strings as code. AI tools
sometimes generate these for dynamic logic. They are always exploitable
when any part of the input is user-influenced.
```javascript
// Dangerous — arbitrary code execution
eval(userExpression);
new Function('return ' + userExpression)();
setTimeout(userString, 1000); // string form acts like eval
// Safe — use a lookup or parser
const operations = { add: (a, b) => a + b, sub: (a, b) => a - b };
const result = operations[operationName](a, b);
```
Search for `eval(`, `new Function(`, and string arguments to `setTimeout`
and `setInterval`. Remove every instance.
### Never use `document.write()`
`document.write()` blocks HTML parsing, can destroy the entire page if
called after load, and enables XSS when used with dynamic content. AI tools
generate it for simple DOM insertion when the DOM API should be used
instead.
```javascript
// Dangerous
document.write('<div>' + userContent + '</div>');
// Safe
const div = document.createElement('div');
div.textContent = userContent;
document.body.appendChild(div);
```
### Check URL handling
```javascript
// Dangerous — allows javascript: protocol
link.href = userProvidedURL;
// Safe — validate protocol first
function isSafeURL(url) {
try {
const parsed = new URL(url, window.location.origin);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
return false;
}
}
if (isSafeURL(userProvidedURL)) {
link.href = userProvidedURL;
}
```
## Input Sanitization
### Validate all user input on both client and server
Client-side validation is for UX. Server-side validation is for security.
Both are required. AI tools almost always generate only client-side validation.
**Check every input for:**
| Input type | What to validate | What AI misses |
|-----------|-----------------|----------------|
| Text | Length limits, allowed characters | Max length enforcement |
| Email | Format, domain | Disposable email detection |
| URLs | Protocol whitelist (http/https only) | `javascript:` protocol |
| Numbers | Range, integer vs float | NaN handling, overflow |
| Files | Type, size, count | MIME type vs extension mismatch |
| Rich text | HTML sanitization | Script injection via attributes |
### Sanitize before storage, escape before display
```javascript
// Input → sanitize → store
const sanitized = DOMPurify.sanitize(userInput);
await saveToDatabase(sanitized);
// Retrieve → escape → display
element.textContent = storedValue; // Auto-escapes HTML
```
## Secrets and Credentials
### No secrets in client-side code — ever
Audit the entire codebase for exposed secrets:
```javascript
// All of these are vulnerabilities in front-end code
const API_KEY = 'sk-abc123...';
const token = 'ghp_xxxxxxxxxxxx';
const password = 'admin123';
const connectionString = 'postgres://user:pass@host/db';
```
**Search for these patterns — they are almost always leaked secrets:**
- `sk-` followed by 20+ alphanumeric characters (OpenAI keys)
- `ghp_` followed by 20+ alphanumeric characters (GitHub personal tokens)
- `api_key` or `api-key` assigned to a string literal
- `secret` assigned to a string literal
- `password` assigned to a string literal
- Connection strings with embedded credentials (`postgres://`, `mongodb://`)
**Also check for:**
- API keys in JavaScript files (including `process.env` that bundles into
the client)
- Tokens in localStorage or sessionStorage without encryption
- Credentials in URL query parameters
- Secrets in HTML meta tags or data attributes
- `.env` files committed to the repository
- Hardcoded OAuth client secrets (client IDs are fine; secrets are not)
### Verify environment variable handling
```javascript
// Dangerous — bundler may inline this into the client build
const secret = process.env.API_SECRET;
// Safe — only use NEXT_PUBLIC_ / VITE_ prefixed vars client-side
const publicKey = process.env.NEXT_PUBLIC_STRIPE_KEY;
```
AI tools frequently put server-side env vars in client-side code because
they don't distinguish between build-time and runtime contexts.
## Content Security Policy (CSP)
### Every page needs a Content-Security-Policy
Check that CSP headers or meta tags exist and are restrictive:
```html
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
">
```
**Red flags in CSP:**
| Directive | Red flag | Why |
|-----------|----------|-----|
| `script-src` | `'unsafe-inline'` | Allows XSS payloads to execute |
| `script-src` | `'unsafe-eval'` | Allows `eval()`, `Function()` |
| `default-src` | `*` | Allows loading from any origin |
| `frame-src` | Missing | Allows clickjacking via iframes |
| `object-src` | Missing | Allows Flash/plugin-based attacks |
AI tools rarely generate CSP headers. If the page has no CSP, that's a
finding.
## Cross-Origin and CORS
### Audit CORS configurations
```javascript
// Dangerous — allows any origin
app.use(cors({ origin: '*' }));
// Safe — whitelist specific origins
app.use(cors({
origin: ['https://example.com', 'https://app.example.com'],
credentials: true,
}));
```
**Check for:**
- `Access-Control-Allow-Origin: *` with `credentials: true` (browser blocks
this, but the intent shows a misunderstanding)
- Wildcard origins in production (acceptable in development only)
- Missing `Access-Control-Allow-Methods` restriction
- `Access-Control-Allow-Headers` that's too permissive
## Authentication and Sessions
### Check token storage
| Storage method | XSS accessible | CSRF vulnerable | Use for |
|---------------|----------------|-----------------|---------|
| `httpOnly` cookie | No | Yes (mitigate with SameSite) | Session tokens |
| `localStorage` | **Yes** | No | Non-sensitive preferences only |
| `sessionStorage` | **Yes** | No | Temporary non-sensitive data only |
| Memory (variable) | No | No | Short-lived tokens during a session |
AI tools default to `localStorage` for auth tokens. This is vulnerable to
XSS — any injected script can steal the token.
### Check authentication flows
- Login form submits over HTTPS only
- Password fields have `autocomplete="current-password"` (login) or
`autocomplete="new-password"` (registration)
- Failed login attempts don't reveal whether the username or password was
wrong ("Invalid credentials" not "User not found")
- Session tokens expire and refresh
- Logout invalidates the token server-side, not just client-side
## CSRF Protection
### Forms that change state need CSRF protection
Any form that creates, updates, or deletes data needs a CSRF token:
```html
<form method="POST" action="/api/update">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<!-- form fields -->
</form>
```
**Also verify:**
- `SameSite=Lax` or `SameSite=Strict` on session cookies
- State-changing operations only accept POST/PUT/DELETE, never GET
- API endpoints validate the `Origin` or `Referer` header
## Third-Party Dependencies
### Audit every external script
```html
<!-- Every external script is an attack surface -->
<script src="https://cdn.example.com/lib.js"></script>
<!-- Use SRI to verify integrity -->
<script src="https://cdn.example.com/lib.js"
integrity="sha384-abc123..."
crossorigin="anonymous"></script>
```
**Check for:**
- External scripts without Subresource Integrity (SRI) hashes
- Scripts loaded from CDNs that could be compromised
- Third-party scripts with access to the full DOM
- Analytics or tracking scripts loaded synchronously
- Outdated libraries with known vulnerabilities
## Sensitive Data Handling
### Don't log sensitive data
```javascript
// Dangerous — tokens appear in browser console and server logs
console.log('Auth response:', authResponse);
console.log('User data:', { email, password, token });
// Safe — log only non-sensitive identifiers
console.log('Auth successful for user:', userId);
```
### Don't expose data in URLs
```javascript
// Dangerous — tokens in URL are logged by proxies, servers, and browser history
window.location = `/dashboard?token=${authToken}`;
// Safe — send tokens in headers
fetch('/dashboard', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
```
## The Security Audit Checklist
Run this checklist on every feature before committing:
- [ ] No `innerHTML` or `outerHTML` with user-provided content
- [ ] No `dangerouslySetInnerHTML` without sanitization
- [ ] No `eval()`, `new Function()`, or string-form `setTimeout`/`setInterval`
- [ ] No `document.write()`
- [ ] All user-facing URLs validated for protocol (no `javascript:`)
- [ ] All user input validated on both client and server
- [ ] No API keys, tokens, or secrets in client-side code
- [ ] No `sk-`, `ghp_`, or other key patterns in source files
- [ ] No secrets in `.env` files committed to the repository
- [ ] Content-Security-Policy header or meta tag is present and restrictive
- [ ] No `'unsafe-inline'` or `'unsafe-eval'` in script-src CSP directive
- [ ] CORS is configured with specific origins, not wildcards
- [ ] Auth tokens stored in httpOnly cookies, not localStorage
- [ ] All forms with side effects have CSRF protection
- [ ] External scripts have Subresource Integrity (SRI) hashes
- [ ] No sensitive data logged to console
- [ ] No sensitive data passed in URL query parameters
- [ ] All cookies have `Secure`, `HttpOnly`, and `SameSite` attributes
A feature is not ship-ready until every box is checked.
## Anti-Patterns
**Never do these:**
- Use `innerHTML` to render user content — use `textContent` or sanitize
- Store auth tokens in `localStorage` — XSS can steal them
- Put API secrets in front-end environment variables — they're in the bundle
- Set `Access-Control-Allow-Origin: *` in production — restrict to known
origins
- Use `eval()` or `new Function()` with any user-influenced input — always
exploitable
- Skip CSP because "it's just a simple page" — XSS doesn't care about
complexity
- Trust client-side validation alone — it's bypassable in seconds
- Log full request/response objects — they contain tokens and PII
- Use GET requests for state-changing operations — they're CSRF-vulnerable
and cached by browsers
- Disable HTTPS in development — train habits that transfer to production