agentleFS
chadixearthchadixearth/graphyloop/skills/frontend-security/SKILL.md

frontend-security

Skill3 starsChanged 10 days agoMITView on GitHub

Use this skill when writing or reviewing client-side code that renders user data, stores tokens, calls an API, or embeds third-party scripts - React/Next/Vue/Svelte or plain DOM. Covers XSS sinks, token storage, environment-variable leakage, CSP and security headers, iframe and postMessage trust, and the checks that catch a public key that is actually a private one.

5 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 5 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never prefix: service-role keys, admin/API secrets, webhook signing secrets, SMTP or DB credentials, private OAuth client secrets.

    line 34
  2. Very specific:

    Never put a token in a URL/query string (logs, referrers, history) or in a client-side cookie without secure.

    line 70
  3. Very specific:

    Every external <script>: pin the version, prefer defer, add integrity + crossorigin when the CDN supports SRI, and add its origin to CSP …

    line 100

“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Frontend security

Server-side review misses the frontend because the frontend "has no secrets" — and then a service-role key ships in a NEXT_PUBLIC_* var, a markdown renderer writes raw HTML into the DOM, and an access token sits in localStorage where any injected script can read it. This skill is the client-side pass: the sinks, the storage, the headers, and what to verify before calling it done.

When to activate

  • Rendering anything a user or an API supplied (HTML, markdown, SVG, URLs, filenames).
  • Auth in the browser: login, logout, token refresh, "remember me", role-based UI.
  • Adding an env var, an analytics/chat/payment script, an iframe, or a postMessage channel.
  • Reviewing a PR that touches any of the above, or a "make it work in the browser" fix.

The five checks, in order

  1. Secrets — no key that grants privilege may reach the bundle.
  2. Sinks — every place a string becomes markup, a URL, or code.
  3. Token storage — where credentials live between requests.
  4. Headers — CSP and friends, set where the app is actually served from.
  5. Third-party — scripts, iframes, and messages you did not write.

1. Secrets in the bundle

  • A var is public the moment its name is prefixed (NEXT_PUBLIC_, VITE_, PUBLIC_, REACT_APP_, EXPO_PUBLIC_) or referenced from a client component. Prefixing a server key does not make it safe — it publishes it.
  • Never prefix: service-role keys, admin/API secrets, webhook signing secrets, SMTP or DB credentials, private OAuth client secrets. See secrets-hygiene.
  • Grep before shipping: NEXT_PUBLIC_.*(SERVICE|SECRET|PRIVATE|ADMIN|TOKEN|PASSWORD), and search the built output (.next/static, dist/assets) for the last 8 characters of any server key you hold. A hit in the build is a leak, not a warning.
  • Anything the browser can call, an attacker can call with the same key. If a key must stay secret, the call belongs in a route handler / server action / edge function, and the client calls that.

2. XSS sinks

SinkRule
dangerouslySetInnerHTML, v-html, {@html}, .innerHTML, outerHTML, insertAdjacentHTML, document.writeOnly with output from a maintained sanitizer (DOMPurify or equivalent), allow-listed tags/attrs, sanitized where it is rendered — not where it was fetched
href / src / action / formaction from dataParse and allow-list the scheme; reject javascript:, data:, vbscript:. Relative or https: only
<a target="_blank">rel="noopener noreferrer"
style / CSS custom properties from dataNever interpolate raw user values into style strings
eval, new Function, setTimeout("string"), dynamic import(userInput)Not with user input. Ever
Inline SVG upload/previewSanitize as HTML — SVG carries <script> and event handlers
JSON.parse(localStorage) then renderTreat as untrusted: another script may have written it
Server-rendered JSON in a <script> tagEscape <, >, &, \u2028/\u2029 or use application/json + textContent

Framework escaping covers interpolation only. JSX escapes {value}; it does not escape dangerouslySetInnerHTML, attribute-injected URLs, or SSR'd script bodies.

Markdown is HTML. marked/markdown-it output must be sanitized unless the renderer is configured to disallow raw HTML.

3. Token and session storage

  • Preferred: httpOnly; secure; sameSite=lax|strict cookie set by the server. JS cannot read it, so an injected script cannot steal it.
  • localStorage/sessionStorage are readable by every script on the origin, including a compromised dependency. Use only for non-credential UI state.
  • In-memory (module/context) plus a refresh call is acceptable for SPAs — it dies with the tab, which is the point.
  • Never put a token in a URL/query string (logs, referrers, history) or in a client-side cookie without secure.
  • Log out means: server invalidates, client clears store and in-memory state. A "logout" that only navigates leaves the session alive.
  • Client-side role checks are UX, not authorization. Hiding an admin button while the API still answers role=admin calls from anyone is an open door — the server must enforce it (see api-hardening).

4. Headers and CSP

Set headers where the app is served (next.config headers, middleware, hosting config, reverse proxy) and verify with a real request, not by reading the config:

curl -sI https://host/ | grep -iE 'content-security-policy|x-frame|x-content-type|referrer|strict-transport|permissions-policy'
  • CSP is the one that actually stops XSS escalation. Aim for default-src 'self', no unsafe-inline/unsafe-eval in scripts (nonce or hash for the framework's inline bootstrap), explicit connect-src, img-src, frame-ancestors 'none' unless embedding is a feature. Ship in Content-Security-Policy-Report-Only first, read the reports, then enforce.
  • X-Content-Type-Options: nosniff · Referrer-Policy: strict-origin-when-cross-origin · Strict-Transport-Security on HTTPS · Permissions-Policy denying camera / microphone / geolocation you do not use.
  • CORS is not a frontend fix. Widening Access-Control-Allow-Origin to * with credentials to "make fetch work" removes a control instead of fixing the bug.

5. Third-party and cross-origin

  • Every external <script>: pin the version, prefer defer, add integrity + crossorigin when the CDN supports SRI, and add its origin to CSP explicitly. A tag-manager container is arbitrary code execution by design — treat it as such.
  • postMessage: always send with an explicit target origin (never '*'), and on receive verify event.origin against an allow-list before reading event.data. Validate the payload shape too.
  • Iframes you embed: sandbox with the minimum tokens, allow list for permissions, referrerpolicy. Untrusted embeds never get allow-scripts allow-same-origin together — that combination lets the frame escape the sandbox.
  • File inputs: validate type and size client-side for UX, and never trust the result. Preview images via URL.createObjectURL, never by injecting markup.

Verification before reporting

  • Build the app, then grep the output for server key fragments and for the prefixed-secret pattern above. Zero hits.
  • curl -sI the running app and paste the security headers you actually got.
  • For each sink you touched: the sanitizer call site, or the reason it is safe.
  • A rendering test with a payload, not a claim: <img src=x onerror=alert(1)>, javascript:alert(1) in a link field, "><script>alert(1)</script> in a search box. Assert escaped text in the DOM.

Reporting

List findings as severity · file:line · what an attacker does · fix. Separate "fixed here" from "needs a server change" (those belong to api-hardening / chadi-backend). If a check could not run — no build, no running server — say which one and why instead of implying it passed.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.