Home Benchmarks Learn Tools News
SPONSOR

AppSignal — Stop vibe-debugging. Every exception, every backtrace, grouped so you see patterns, not noise.

↗
Skills · Security Review
Review skill · v2 · 18-point audit

Catch the vulns before they ship.

Runs inside your agent. Audits 18 security patterns AI gets wrong by default. Scores the result A–F.

Install the skill → Read SKILL.md
18
Audit checks
6
AI tools supported
0
Dependencies
agent — review-security live
$ agent run --skill review-security
▼ security review starting · 18 checks · target: src/
no innerHTML with user input PASS
no eval() / document.write() PASS
exposed key in lib/api.ts:8 CRIT
→ const KEY = 'sk-proj-9f2k...'
token in localStorage at auth.ts:12 HIGH
csp present + restrictive PASS
cors whitelist (no wildcard) PASS
csrf token on POST forms PASS
score 73 / 100 C
→ fix 2 critical findings before shipping. autopatch? [Y/n]
Works with
  • Cursor
  • Claude Code
  • Codex CLI
  • Windsurf
  • GitHub Copilot
  • Gemini CLI
What it catches

The vulns AI ships by default.

review-security / checks SKILL.md · 18 checks · OWASP-aligned
checks/
xss.diff •
−render.tsx Before
// any <script> in the response runs
el.innerHTML = userContent;
container.innerHTML = `<div>${comment}</div>`;
<div dangerouslySetInnerHTML={{ __html: bio }} />
+render.tsx After
el.textContent = userContent;
 
const div = document.createElement('div');
div.textContent = comment;
container.appendChild(div);
 
<div>{bio}</div>
− treats user content as HTML · XSS on render → + text as text · framework escapes · injection-proof
secrets.diff •
−config.ts Before
// shipped to every browser
const KEY = 'sk-proj-9f2k7Pz...';
const token = 'ghp_xxxxxxxxxxxx';
const secret = process.env.API_SECRET;
+config.ts After
// only NEXT_PUBLIC_ / VITE_ vars hit the client
const publicKey = process.env.NEXT_PUBLIC_STRIPE_KEY;
 
// secrets stay server-side, behind an API route
await fetch('/api/charge', { method: 'POST' });
− sk- / ghp_ keys in the bundle · public forever → + server-only · prefix-gated · proxied via /api
auth-storage.diff •
−auth.ts Before
// any injected script can grab this
localStorage.setItem('token', jwt);
sessionStorage.setItem('session', sessionId);
 
// no SameSite, no Secure, no HttpOnly
document.cookie = `auth=${jwt}`;
+session.http After
// server response, not client code
Set-Cookie: auth=jwt;
HttpOnly;
Secure;
SameSite=Lax;
Path=/;
Max-Age=3600
− token in JS reach · one XSS = takeover → + HttpOnly + Secure + SameSite · out of JS reach
headers.diff •
−server.ts Before
// no CSP at all
 
// any origin can hit the API
app.use(cors({ origin: '*' }));
 
// no SRI on third-party scripts
<script src="https://cdn.example.com/lib.js"></script>
+server.ts After
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; object-src 'none';">
 
app.use(cors({ origin: ['https://example.com'] }));
 
<script src="https://cdn.example.com/lib.js"
integrity="sha384-abc123..."
crossorigin="anonymous"></script>
− no CSP · CORS * · CDN script unverified → + CSP · allow-list CORS · SRI on every CDN
Benchmarked

Proof, not vibes.

Auth Form brief · Claude Opus 4.7 · with skill vs. without · 2026-04-22
100
Best Practices 73 → 100 baseline +27
0
Critical findings 3 → 0 baseline −3
6
Secrets caught Avg flagged per run
91
Security grade · A vs C · 71 baseline +20
0–49 50–89 90–100
Single-run comparison. Same model and prompt, scored on Lighthouse + an internal rubric.
Install

One file. Six tools. Zero ceremony.

One Markdown file, zero dependencies. Pick your tool below.

1Drop this in

Project: .cursor/skills/review-security.md

2Or fetch it directly
curl -fsSL https://webdeveloper.com/skills/review-security/SKILL.md -o .cursor/skills/review-security.md

Restart Cursor. The next time you ask the agent to review code, it’ll run the 18-point security audit and grade the output.

1Drop this in

User-level: ~/.claude/skills/review-security/SKILL.md

2Or fetch it directly
mkdir -p ~/.claude/skills/review-security && curl -fsSL https://webdeveloper.com/skills/review-security/SKILL.md -o ~/.claude/skills/review-security/SKILL.md

Claude Code auto-discovers skills in ~/.claude/skills/. Available across every project on this machine.

1Drop this in

Project: AGENTS.md (append the SKILL contents)

2Or fetch it directly
curl -fsSL https://webdeveloper.com/skills/review-security/SKILL.md >> AGENTS.md

Codex CLI reads AGENTS.md automatically when you run it from the project root.

1Drop this in

Project: .windsurf/rules/review-security.md

2Or fetch it directly
mkdir -p .windsurf/rules && curl -fsSL https://webdeveloper.com/skills/review-security/SKILL.md -o .windsurf/rules/review-security.md

Windsurf loads project rules on every Cascade run.

1Drop this in

Project: .github/copilot-instructions.md (append)

2Or fetch it directly
mkdir -p .github && curl -fsSL https://webdeveloper.com/skills/review-security/SKILL.md >> .github/copilot-instructions.md

Copilot reads .github/copilot-instructions.md as project-wide context.

1Drop this in

Project: .gemini/skills/review-security.md

2Or fetch it directly
mkdir -p .gemini/skills && curl -fsSL https://webdeveloper.com/skills/review-security/SKILL.md -o .gemini/skills/review-security.md

Gemini CLI auto-loads project skills on the next run.

The full SKILL.md

576 lines · plain Markdown · MIT-licensed
SKILL.md
---
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
Pair it

Stack it with the rest of the suite.

Review07 Pre-Deploy Review

26-point audit of error handling, debug artifacts, hallucinated APIs, and a11y smells — the broader pre-flight checklist.

↗
Review09 Testing Review

Behavior-first tests, error and edge-case coverage, accessibility contract tests, mock guidelines.

↗
Front-end06 Accessibility

WCAG 2.2 AA, keyboard nav, focus management, contrast, screen reader patterns, form accessibility.

↗

Changelog

V2 April 15, 2026
Added explicit eval() and document.write() detection sections, specific secret patterns (sk-, ghp_, api_key), and expanded the security checklist from 15 to 18 items.
V1 April 15, 2026
Initial skill covering XSS prevention, input sanitization, secret exposure auditing, Content Security Policy, CORS configuration, authentication flows, CSRF protection, third-party dependency auditing, and the security audit checklist.

FAQ

What security vulnerabilities does this skill catch?

An 18-point audit covering XSS via innerHTML and dangerouslySetInnerHTML, eval() and document.write() usage, input sanitization gaps, exposed API keys and secrets (including sk-, ghp_ patterns) in client-side code, missing or permissive Content Security Policy, overly broad CORS configurations, insecure token storage in localStorage, missing CSRF protection, and third-party scripts without Subresource Integrity hashes. The agent then scores the code A–F so you know if it’s ship-ready.

Why is AI-generated code particularly vulnerable to security issues?

AI coding tools default to the simplest working implementation, which is often the least secure. They use innerHTML instead of textContent, store tokens in localStorage instead of httpOnly cookies, hardcode API keys, generate permissive CORS configurations, and skip Content Security Policy headers entirely. These patterns work in development but create real attack surfaces in production.

How should auth tokens be stored securely?

Auth tokens should be stored in httpOnly cookies with Secure, SameSite=Lax or Strict attributes. localStorage and sessionStorage are accessible to any JavaScript running on the page, making them vulnerable to XSS attacks. The skill covers token storage, session management, and authentication flow security patterns.

Which AI coding tools is this compatible with?

Cursor, Claude Code, Codex CLI, Windsurf, GitHub Copilot, and Gemini CLI. The skill is a single Markdown file and ships in the native format for each tool with one-click copy.

STATUS ● BUILDING THE FUTURE
MISSION MAKE AI SHIP BETTER CODE.
VERSION BETA 3.0

MAKE AI SHIP BETTER CODE.

@WEBDEVELOPERHQ ↗
TERMS / PRIVACY
FRIENDS
Authentic Jobs ↗
Web Reference ↗
Ready.dev ↗
Fullres ↗
© 2026 WEB DEVELOPER / ALL RIGHTS RESERVED