Frontend render security
Skill event4u-app/agent-config/dist/agent-src/skills/frontend-render-security
Writing/reviewing client-side UI (React/Vue/vanilla) — insecure-render + client-trust gaps AI ships: XSS via innerHTML, client secrets, client-only auth, CORS wildcard, token in localStorageFrom its SKILL.md
npx -y skills add event4u-app/agent-config --skill frontend-render-securityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.1 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it
frontend-render-security
AI optimizes for the shortest code producing the requested visible behavior and omits the invisible defensive layer — the sanitizer, origin check, server-side gate, security header. Large samples put XSS in a majority of AI frontend code, ~2.74× more than human; every agent in the Tenzai benchmark shipped client-side flaws. High-precision, grep-catchable patterns — stop them at authoring time.
When to use
- Writing/modifying a component, template, or client-side script that renders data, calls an API, handles auth, or reads the URL /
postMessage. - Reviewing an AI-authored frontend diff.
- Editing
.tsx/.jsx/.vue/.svelte/.htmlor client-side.js/.ts.
Do NOT use when: change is server-only, CLI, or non-UI — route to security / security-sensitive-stop.
The Iron Law
NEVER RENDER NON-CONSTANT INPUT INTO AN HTML/JS SINK WITHOUT ENCODING OR SANITIZING.
THE CLIENT IS UNTRUSTED — EVERY CLIENT GATE NEEDS A SERVER-SIDE TWIN.
NO SECRET, PRIVILEGED KEY, OR SESSION TOKEN LIVES IN CLIENT CODE OR localStorage.
Procedure
- Identify the sinks in the diff: HTML/JS render points, API calls, auth/role checks, URL /
postMessagereads, secret/token usage. - Run the backstop greps below; for each hit, apply the matching fix from the patterns table.
- For every client-side gate, confirm a server-side twin exists (the client is never the security boundary).
- Emit the per-diff verdict (Output format); surface any unresolved hit to the user.
The patterns AI ships by default
| Pattern | Why dangerous | Do instead |
|---|---|---|
dangerouslySetInnerHTML / v-html / .innerHTML = on non-constant value | Stored/DOM XSS (CWE-79) | render as text, or sanitize (DOMPurify) if HTML required |
Secret / API key inline or in NEXT_PUBLIC_* / VITE_* | Bundler inlines it into shipped JS (CWE-798) | call third party via backend-for-frontend proxy; only truly-public values get a public prefix |
| Client-side-only auth / role / validation | Bypassable via dev tools or proxy (CWE-602) | client gate = UX only; enforce server-side |
location.* / query param → HTML or JS sink | DOM XSS (CWE-79) | textContent; never pass URL data into an HTML/exec sink |
CORS origin: '*' (esp. with credentials) | Any site reads authenticated responses (CWE-942) | explicit origin allow-list; never reflect Origin on credentialed routes |
Token / JWT in localStorage / sessionStorage | XSS-exfiltratable (CWE-522) | HttpOnly; Secure; SameSite cookie |
addEventListener('message', …) without event.origin check | Any embedder drives the handler (CWE-346) | strict-equality event.origin vs allow-list first |
redirect / next param → location / router.push | Open redirect / phishing (CWE-601) | relative-path or allow-listed-host only; reject external / non-http schemes |
eval / new Function / string-setTimeout on input | Eval injection / RCE (CWE-95) | a parser or explicit allow-list |
target="_blank" without rel="noopener" | Reverse tabnabbing (CWE-1022) | add rel="noopener noreferrer" |
| Missing CSP; missing alt/label/contrast | No XSS second line of defense; a11y failures | strict CSP (no unsafe-inline); alt text, labels, AA contrast |
Backstop greps
Run before committing frontend changes; each returns zero (or every hit is read + justified):
# Insecure render + eval sinks
rg -n 'dangerouslySetInnerHTML|v-html|\.innerHTML\s*=|document\.write\(|\beval\(|new Function\('
# Client secrets / token storage
rg -n 'NEXT_PUBLIC_.*(SECRET|KEY|TOKEN|PASSWORD)|VITE_.*(SECRET|KEY)|localStorage\.setItem\([^)]*[Tt]oken'
# Wildcard CORS + unchecked postMessage + open redirect
rg -n "origin:\s*['\"]\*['\"]|Access-Control-Allow-Origin.*\*"
rg -n "addEventListener\(\s*['\"]message['\"]" # then confirm each checks event.origin
# Unhardened external links
rg -n 'target=["'\'']_blank["'\'']' # then confirm rel="noopener" present
Output format
- A per-diff verdict listing each pattern class checked + result (
clean/hit at file:line → fixed by <change>). - The backstop greps run, with results.
- For any client gate added, file:line of its server-side twin (or an explicit note the server check already exists + where).
Gotcha
- Modern browsers default
_blanktonoopener, but legacy/embedded webviews don't — keep therelfor portability. NEXT_PUBLIC_/VITE_on a genuinely public value (a publishable analytics ID) is fine; the violation is a sensitive name behind that prefix. Read the name, don't blanket-block the prefix.- A sanitizer (DOMPurify) is the fix only when HTML output is actually required; if plain text suffices, render text and skip the dependency.
- Client-side validation isn't wrong — it's wrong as the only enforcement. Keep it for UX; add the server twin.
Do NOT
- Do NOT render user/DB HTML through
dangerouslySetInnerHTML/v-html/innerHTMLwithout a sanitizer. - Do NOT put a secret, privileged key, or session token in client code or web storage.
- Do NOT treat a client-side role/auth check as a security boundary.
- Do NOT set
origin: '*'on a credentialed endpoint. - Do NOT auto-strip a grep hit without reading it — some innerHTML uses are on constant, trusted markup.
Auto-trigger keywords
- frontend security
- XSS
- dangerouslySetInnerHTML
- client-side secret
- insecure render
See also
ai-code-blindspots— the surface→controls checklist that routes here.senior-engineering-discipline— anchor rule.security,defense-in-depth,accessibility-auditor,secrets-management.
What ships with it: 1 file
2.1 KB alongside SKILL.md
evals/
- triggers.json2.1 KB