agentsclimarketplace

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

Install
npx -y skills add event4u-app/agent-config --skill frontend-render-security

Assembled 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 / .html or 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

  1. Identify the sinks in the diff: HTML/JS render points, API calls, auth/role checks, URL / postMessage reads, secret/token usage.
  2. Run the backstop greps below; for each hit, apply the matching fix from the patterns table.
  3. For every client-side gate, confirm a server-side twin exists (the client is never the security boundary).
  4. Emit the per-diff verdict (Output format); surface any unresolved hit to the user.

The patterns AI ships by default

PatternWhy dangerousDo instead
dangerouslySetInnerHTML / v-html / .innerHTML = on non-constant valueStored/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 / validationBypassable via dev tools or proxy (CWE-602)client gate = UX only; enforce server-side
location.* / query param → HTML or JS sinkDOM 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 / sessionStorageXSS-exfiltratable (CWE-522)HttpOnly; Secure; SameSite cookie
addEventListener('message', …) without event.origin checkAny embedder drives the handler (CWE-346)strict-equality event.origin vs allow-list first
redirect / next param → location / router.pushOpen redirect / phishing (CWE-601)relative-path or allow-listed-host only; reject external / non-http schemes
eval / new Function / string-setTimeout on inputEval 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/contrastNo XSS second line of defense; a11y failuresstrict 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

  1. A per-diff verdict listing each pattern class checked + result (clean / hit at file:line → fixed by <change>).
  2. The backstop greps run, with results.
  3. 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 _blank to noopener, but legacy/embedded webviews don't — keep the rel for 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/innerHTML without 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

What ships with it: 1 file

2.1 KB alongside SKILL.md

evals/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.