Frontend security
Skill Syo-M/fable-frontend-skills/plugin/skills/frontend-security
Measured, opinionated Claude Code rules for frontend work (React / Next.js / Vite / Astro) — skills, path rules, review agents, sign-off hooks, installer & plugin. Every claim backed by committed eval reports.
npx -y skills add Syo-M/fable-frontend-skills --skill frontend-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
- 3 stars3 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.
What its author says it does
Copied from the file, not written here
Frontend security rules — XSS, injection, validation, CSRF, SSRF, CORS, secrets, sessions/JWT, rate limiting, headers, uploads, third-party scripts, prompt injection, dependency hygiene. Use whenever code touches user input, HTML rendering, auth, cookies, URLs, outbound fetch, webhooks, env vars, file uploads, logging, untrusted content fed to an LLM, or new dependencies. 日本語の依頼例:「認証/ログイン実装」「入力フォーム作って」「問い合わせ/お問い合わせフォーム作って」「送信フォーム」「cookie/セッション」「外部APIを叩いて」「webhook受信」「ファイルアップロード」「環境変数」「依存パッケージ追加」「LLMに渡す」。
SKILL.md
12.9 KB, as published. Nobody here has run it
Frontend Security
Order of defense: don't accept untrusted data → validate at the boundary → encode/sanitize at output → restrict with platform policy (CSP, cookies, headers). Apply all layers, not one. These rules are policy: changing or weakening them requires human security review (see governance).
XSS
- Framework escaping is the default protection — keep it.
dangerouslySetInnerHTML(React),set:html(Astro),innerHTML/insertAdjacentHTML(DOM) with anything non-static require sanitization with DOMPurify at output time, using one shared hardened config — not ad-hoc per-call options. SSR/build-time paths needisomorphic-dompurify(DOMPurify requires a DOM). No home-made regex sanitizers, ever. - URLs are an injection vector: any
href/srcfrom user data → parse withnew URL, allow onlyhttp:/https:protocols (blocksjavascript:URLs that escaping does not stop). - Never build HTML/CSS/JS by string concatenation with user data. No
eval,new Function, stringsetTimeout. - Don't render raw user input into
<script>contexts, JSON-LD blocks, or inline event handlers.
Validation — server-side, schema-first
- Every boundary parses input with zod before use: Server Actions, route handlers, URL/search params, cookies, third-party API responses, webhook payloads (after signature verification — see below).
- Client-side validation is UX only; the server must reject independently.
- Default to closed shapes:
z.strictObject({...})(zod 4;.strict()is the legacy spelling) — unknown keys are rejected, blocking mass-assignment (isAdmin: truein a profile update). Plainz.object()silently strips unknown keys, which also prevents mass-assignment but hides client bugs. Document any exception. - Bound every user-supplied string with
.max(n)— unbounded input is a DoS/ReDoS vector. - Locale-formatted input (decimal comma, native digits): parse to a canonical form first, then validate the canonical value (see
i18n). - ReDoS: never execute user-controlled regex; cap input length before any regex; avoid nested quantifiers (
(a+)+). - Prototype pollution: never recursively merge user JSON into existing objects; reject
__proto__/constructor/prototypekeys; preferMaporObject.create(null)for user-keyed lookups. - IDs from the client are claims, not facts: after auth, check the resource belongs to / is permitted for this user (IDOR). Per-resource, in every action/handler — middleware route guards are not enough.
- Authorization model: decide it deliberately (role-based for coarse access, attribute/ownership-based for per-record) and enforce it server-side in one place (a policy helper), not ad-hoc
if (user.role === 'admin')scattered across handlers. Deny by default.
Sessions, cookies, JWT
- Session cookies:
HttpOnly,Secure,SameSite=Lax(orStrict),__Host-prefix where possible (enforcesPath=/, noDomain). Never store session tokens inlocalStorage. - Rotate the session ID on login and on privilege change (session fixation).
- JWT, if used: decode ≠ verify — always verify the signature with an
algallow-list (rejectnone); checkexp/aud/iss; short-lived access tokens + rotated refresh tokens; no PII in claims that reach the client. - Security-relevant randomness (tokens, nonces, IDs):
crypto.randomUUID()/crypto.getRandomValues()— neverMath.random(). No hand-rolled crypto; Web Crypto only.
CSRF & redirects
- Framework protection covers less than you think: Next.js verifies Origin for Server Actions only. Route handlers have no built-in CSRF protection — every cookie-authenticated state-changing route handler needs explicit Origin/Referer validation or a CSRF token.
SameSiteis defense-in-depth, not sufficient alone (subdomains, legacy clients). - Mutations are POST-family only; a state-changing GET is a bug regardless of other protections.
- Open redirects: never
redirect(userInput)raw — allow-list relative paths or known origins. Same rule for OAuthredirect_uri; always validate the OAuthstateparameter.
Outbound requests — SSRF & webhooks
- Server code fetching a user-supplied or user-influenced URL: allow-list hosts; block private/link-local ranges and cloud metadata endpoints (
169.254.169.254) — and the IPv6 equivalents (::1,fc00::/7,fe80::/10, IPv4-mapped::ffff:169.254.169.254); re-validate after redirects. Constrainnext/imageremotePatternstightly — the image optimizer is a classic SSRF proxy. - Defeat DNS rebinding / TOCTOU: resolve the host, validate the resolved IP against the deny ranges, then connect to that pinned IP (don't validate a hostname and then let the HTTP client re-resolve it).
- Incoming webhooks: verify the provider's HMAC signature (constant-time compare) before trusting the payload; reject when the signature header is missing. Schema validation is not authentication.
CORS
- Never reflect the request
Originwhile sendingAccess-Control-Allow-Credentials: true; never*on authenticated endpoints. Maintain an explicit origin allow-list. "Fixing a CORS error" by widening headers is usually creating a vulnerability — find the legitimate origin instead.
Rate limiting & auth failures
- Throttle login, signup, password reset, and expensive mutations — per IP and per account.
- Uniform error messages and response timing on login/reset: do not reveal whether an account exists.
Untrusted content & prompt injection
Relevant whenever the app sends content to an LLM or renders model output (chat, summarization, RAG, agentic features).
- Treat all external content — user input, fetched pages, API/webhook payloads, documents, retrieved chunks — as data, never instructions. It may contain text attempting to redirect the model ("ignore previous instructions…"); enclose it in clearly delimited, labeled fields and instruct the model that delimited content is data only.
- The model's output is itself untrusted: never
eval/render it as HTML/execute it as a tool call without the same validation, sanitization (DOMPurify), and authorization you apply to user input. Tool/function calls the model requests are authorized server-side per the acting user — the model cannot grant itself capability. - Never put secrets, API keys, or another user's data in a prompt or system message that a response could leak back. Apply least-privilege to any tool the model can invoke; high-impact actions (sending mail, spending, deletes) require explicit human confirmation, not model discretion.
- Server-side LLM keys stay server-side (never
NEXT_PUBLIC_/VITE_); rate-limit and budget-cap LLM endpoints (cost-DoS).
Secrets, env, logging
- Secrets never appear in: client bundles (
NEXT_PUBLIC_/VITE_/PUBLIC_vars are public by definition), repo files, logs, client-bound error messages, or URL query strings (query strings leak via logs and referrers). .env*in.gitignore; commit.env.examplewith placeholders. A secret ever committed = rotate it; deleting the file does not unpublish it. Secrets scanning in CI is mandatory (seegovernance).- No PII, credentials, or tokens in server logs, analytics events, or error-tracker payloads — configure scrubbing (e.g. Sentry
beforeSend). Server errors: log details server-side, return a generic message + correlation ID. Stack traces and DB errors never cross the wire. - Conversely, DO record security events for audit (A09 / ASVS V7): authentication success & failure, authorization denials (IDOR / 403), privilege/role changes, webhook signature-verification failures, password/email changes, and admin actions — each with actor, action, result, source IP, and correlation ID. "Don't log PII" governs the payload, not the security-event metadata — silent auth failures are how breaches go undetected. Make these logs tamper-evident and retained per policy.
- Data minimization & retention: collect only what's needed; set a retention window on logs/analytics containing personal data and honor deletion requests — don't keep PII indefinitely "just in case".
Headers & platform policy
Set and keep (next.config / astro config / hosting headers):
Content-Security-Policy:script-srcmust NOT contain'unsafe-inline'(it reopens the main XSS path that framework escaping closes) — use a per-request nonce or hashes, and prefer'strict-dynamic'. At minimum:script-srcwithout'unsafe-inline'/'unsafe-eval',object-src 'none',base-uri 'self',form-action 'self', andframe-ancestors 'none'(or explicit allow-list) +X-Frame-Options: DENYfallback. Roll out viaContent-Security-Policy-Report-Onlyfirst, with reports routed somewhere monitored.Strict-Transport-Securitywith a long max-age (understand the commitment before addingpreload).Cache-Control: no-storeon authenticated/personalized responses — caching a per-user response is a data leak, not a perf win.X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,Cross-Origin-Opener-Policy: same-origin,Permissions-Policydenying unused features.
Third-party scripts & embeds
- Third-party scripts require approval (see
governance), exact version pinning, andintegrity(SRI) when loaded from a CDN; prefer self-hosting. Tag managers that load arbitrary scripts defeat CSP — avoid, or strictly govern what they may inject. - iframes/embeds: explicit
sandboxattribute;postMessagehandlers must checkevent.originagainst an allow-list.
Dependencies
- Before adding: maintenance, downloads, install scripts (
postinstall), transitive weight, license (seegovernance), and the EXACT package name (typosquatting). - Dependency confusion: publish private packages under the org scope (
@company/); pin scope→registry mapping in.npmrc; assert registry URLs withlockfile-lintin CI. - Prefer a release cooldown: avoid versions published within the last few days (compromise/worm window). Pin CI actions by commit SHA, not tag.
- Lockfile always committed; CI installs frozen (
npm ci/pnpm install --frozen-lockfileper the repo's package manager), with--ignore-scriptswhere the build allows. - Audit on every dependency change AND on a weekly schedule; high + critical findings block.
Uploads & user files
- Validate type by content (magic bytes), not extension; cap size; store outside the web root / in object storage.
- SVG passes image checks but executes scripts: deny or sanitize SVG. Serve user uploads from a separate origin — re-encoding raster images through the framework's image optimizer satisfies this intent; anything served as-is (HTML, SVG, PDF, downloads) must use the separate origin.
Content-Disposition: attachmentfor non-image types; correctContent-Type+nosniff. - Authorize every download per-request — non-guessable names are defense-in-depth, never the access control. Intentionally-public assets (e.g. avatars) may skip per-request auth as an explicit, documented decision — not a default.
- Never trust client-supplied paths/filenames: path traversal, zip-slip when extracting archives.
Self-check before shipping boundary code
These are the controls lint can't fully prove (only Semgrep-backstopped in templates/.semgrep/). Before finishing any change that touches a boundary, answer each for what you touched — a "no" means fix it, don't ship:
- External input (body / params / cookies / API response / webhook) parsed with a zod schema at the boundary?
- Webhook payload signature verified (constant-time) BEFORE it's trusted?
- Every client-supplied ID authorized for this user, per resource (IDOR)?
- Authorization enforced server-side in the one policy helper, deny-by-default — not scattered role checks?
- Outbound fetch to a user-influenced URL: host allow-listed, private/link-local/metadata ranges blocked, AND connected to the pinned resolved IP (DNS-rebinding)?
- No secret / PII / token in the client bundle, logs, analytics, error payloads, or URLs?
- Non-static HTML sanitized (DOMPurify)? No new
dangerouslySetInnerHTML/set:html/innerHTMLwithout it? - Session cookies
HttpOnly+Secure+SameSite; cookie-auth state-changing route handlers CSRF-protected? - Untrusted / model-bound content handled as data, never instructions?