plugins/frontend-toolkit/skills/security-audit/SKILL.md
OWASP Top 10 audit for frontend — XSS via dangerouslySetInnerHTML, env-var leaks, token storage, CSRF, broken access control (IDOR), open redirect, CSP, Supabase RLS, CORS, Zod env validation. Use when adding auth, after handling external input, before shipping, or quarterly. Not for the initial env-validation setup (use developer-experience) or wiring npm audit / CSP regression tests into CI (use cicd-pipeline).
npx skillsauth add jaykim88/claude-ai-engineering security-auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Identify and fix frontend-side security vulnerabilities using OWASP Top 10 as the checklist. Defense-in-depth: no single fix solves all attacks — combine output encoding, CSP, sanitization, and server-side enforcement.
Universal — OWASP, CSP, RLS, env-leak detection, and auth-token storage rules apply to all stacks; only the framework-specific syntax differs (NEXT_PUBLIC_ vs VITE_ vs PUBLIC_).
Context-aware output encoding (OWASP core teaching)
<div>{userInput}</div> is safe)style props<script>, eval(), setTimeout(string)style attribute as a string — use object syntaxjavascript: / data: for hrefs); allowlist redirect targets — a ?redirect= / returnTo param fed into router.push() or <a href> is an open-redirect (phishing) vectorSanitize any raw-HTML injection sink before render — better, avoid raw-HTML injection entirely (use text content)
dangerouslySetInnerHTML + DOMPurify — see Implementation)Never put secrets in client-bundled env vars; audit the client-prefixed vars for keys/tokens
console.log, secrets passed in URL query params (logged in server/referrer/analytics)NEXT_PUBLIC_* — see Implementation)Audit token storage
localStorage or sessionStorage (XSS-readable)grep -rn 'localStorage.*token\|sessionStorage.*token' src/4b. Protect cookie-based auth against CSRF
SameSite=Lax (or Strict) blocks most cases but NOT all (SameSite=None integrations, some cross-site POST contexts)Origin / Sec-Fetch-Site checkAuthorization) aren't CSRF-prone — but they live in JS memory, so don't reintroduce the localStorage problem from step 4Detect code-execution sinks
eval(), new Function() — should never appearsetTimeout('string'), setInterval('string') (string form, not function form)Verify CSP headers
Content-Security-Policy set with at minimum:
script-src 'self' [trusted CDNs] (no 'unsafe-inline' or 'unsafe-eval')object-src 'none'frame-ancestors 'none' (or specific allowlist)Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default
Forces all DOM sink writes (innerHTML, etc.) to go through a Trusted Types policy.integrity + crossorigin) to third-party CDN <script>/<link> so a compromised CDN can't swap in malicious code (coordinate with third-party-scripts)Enforce authentication AND authorization server-side — client checks are decorative/bypassable
if (!user) redirect() is always bypassable; the server must verify the session on every protected route/api/orders/123 must confirm order 123 belongs to the caller (IDOR / broken object-level access control). The UI hiding a link is not access controlrequireUser() + per-resource ownership check — see Implementation)Enable row-level security on every user-data table; verify policies aren't using (true); keep public tables few
using (true))CORS audit
Access-Control-Allow-Origin: * for endpoints that accept credentialsValidate env at build via a typed schema (fail the build, not production runtime)
@t3-oss/env-nextjs — see Implementation)npm audit — see Implementation)| ❌ Anti-pattern | ✅ Correct |
|---|---|
| Auth token in localStorage | HttpOnly + Secure + SameSite cookie |
| NEXT_PUBLIC_API_SECRET in env | Server-only env (no NEXT_PUBLIC_ prefix) |
| dangerouslySetInnerHTML={{__html: userInput}} | DOMPurify.sanitize(userInput) first, or refactor to JSX text |
| Client-only if (!user) redirect() | Server-side requireUser() in Server Component / Route Handler |
| /api/orders/:id returns any user's order | Verify object ownership per request (RLS or explicit check) |
| State-changing cookie request with no CSRF defense | SameSite + Origin/Sec-Fetch-Site or CSRF token |
| router.push(searchParams.get('redirect')) | Allowlist redirect targets |
| Access-Control-Allow-Origin: * for credentialed endpoints | Explicit origin allowlist |
| process.env.X accessed directly | Typed env.X via @t3-oss/env-nextjs |
| Tier | Examples | Action SLA |
|---|---|---|
| Critical | Auth token in localStorage; missing RLS on user-data table; broken object-level access control (IDOR — user reads/modifies another user's data); eval() on user input | Block release; fix immediately |
| Major | No CSRF defense on cookie-auth state-changing requests; open redirect via unvalidated param; missing CSP script-src restriction; CORS wildcard; NEXT_PUBLIC_* leak | Fix this sprint |
| Minor | Missing frame-ancestors; public production source maps; missing SRI on CDN scripts; outdated dep with no known exploit | Schedule within 2 sprints |
dangerouslySetInnerHTML sanitized via DOMPurifyNEXT_PUBLIC_* env varsSameSite + origin/token check)unsafe-inline/unsafe-eval for script-src)returnTo params allowlisted (no open redirect)npm audit fix (can introduce breaking changes)docs/security-audit-YYYY-MM-DD.md with sections:
## Summary — counts by tier (Critical / Major / Minor)## Critical findings — per finding: file:line, category, exploit scenario, fix## Major findings — same format## Minor findings — same format## Verification — tools used, commands run, validation loop resultsfix(security): <description> [severity: critical|major|minor]next.config.ts (or equivalent) with a comment linking to the audit reportNEXT_PUBLIC_* — never put secrets hereDOMPurify for dangerouslySetInnerHTMLrequireUser() middlewareresource.userId === session.user.id in every handler (no IDOR); RLS for Supabase tablesSameSite=Lax/Strict cookies + verify Origin / Sec-Fetch-Site (or a CSRF token) in Server Actions / Route Handlers for mutationsproductionBrowserSourceMaps: false (default) — don't ship original source to the publicnext.config.ts headers() function; nonce-based for inline scripts@t3-oss/env-nextjs (Zod schemas)NUXT_PUBLIC_*; sanitization v-html + DOMPurify; CSP via nuxt.config.ts routeRulesPUBLIC_*; sanitization in +page.svelte with DOMPurify; CSP via svelte.config.js csp optionVITE_*DomSanitizer (don't bypass); CSP via meta tag or server headeraccessibility-audit — some aria attrs (aria-*) carry security weight toodeveloper-experience — env validation (@t3-oss/env-nextjs) is set up therethird-party-scripts — SRI, CSP, and consent for external/CDN scriptscicd-pipeline — wire npm audit and CSP regression tests into CIrequireUser(), env validation at build) is what actually secures the application. Two easily-missed gaps: cookie auth (the recommended token storage) needs CSRF defense (SameSite + origin/token), and authentication ≠ authorization — verify object ownership per request or you ship IDOR (OWASP's #1).development
Audit and optimize third-party scripts — analytics, tag managers, chat widgets, embeds — with the right loading strategy, performance budget, facades, and CSP/consent controls. Use when adding a script, when TBT/INP regress, when a GDPR/CCPA consent requirement arises, or before shipping. Not for first-party bundle size (use bundle-optimization) or broad Core Web Vitals diagnosis (use rendering-performance).
development
Apply the Testing Trophy (mostly integration tests with RTL + MSW, sparing E2E with Playwright) and set coverage thresholds. Use before new feature work, after bug fixes, when CI coverage falls below target, or when tests are flaky or break on every refactor. Not for wiring coverage gates + Playwright into the GitHub Actions matrix (use cicd-pipeline) or auditing WCAG a11y compliance (use accessibility-audit).
development
Inventory and prioritize technical debt — TODO/FIXME/HACK, any usage, deprecated APIs, untested logic — with impact × effort matrix. Use at quarter start, before a refactoring sprint, when a new teammate joins, or when feature velocity slows. Not for actually paying down debt (use code-refactoring) or recording a migration approach (use decision-records) — this only inventories and prioritizes.
development
Decision framework for choosing the right state location — URL, server cache, local component, or shared/global store. Use when state-sync bugs appear, prop drilling gets deep (3+ levels), filters/tabs lose state on reload, or quarterly review. Not for form state specifically (use form-ux) or when the state is actually server data (use api-caching-optimization).