Frontend security
Skill Syo-M/codex-frontend-skills/.agents/skills/frontend-security
Frontend/BFF security for forms and external input, auth, cookies, HTML, URLs, fetch, CORS, webhooks, uploads, secrets, logging, dependencies, and LLM-bound content. 日本語の依頼例:「認証実装」「問い合わせフォーム」「外部API」「webhook」「ログ」「LLMに渡す」「依存追加」。From its SKILL.md
npx -y skills add Syo-M/codex-frontend-skills --skill frontend-securityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 28 days oldThe repository was created 28 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 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
13.8 KB, ~3.0k tokens by cl100k_base, 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. - For identifiers, names, and other strings where surrounding whitespace has no meaning, normalize before validating:
z.string().trim().min(1).max(n). Explicitly test a whitespace-only value;.min(1)alone accepts a single space. Do not trim passwords, signed payloads, or fields where whitespace is meaningful. - Locale-formatted input (decimal comma, native digits): parse to a canonical form first, then validate the canonical value (see
i18n). - Validator-native error text is internal implementation detail and may leak the wrong language. For user-visible errors, map stable issue/path combinations to the active message catalog and test every supported locale; see references/localized-validation.md.
- 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: prefer not accepting arbitrary URLs. When the feature genuinely requires them, use a reviewed network client or egress proxy that resolves once, rejects every private/link-local/metadata result, pins the approved address through connect, preserves the original hostname for TLS SNI/Host validation, and revalidates every redirect. Do not hand-roll this with a naive hostname check followed by ordinary
fetch. - 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: record only scrubbed diagnostics server-side, then 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 a policy-approved actor identifier, action, result, correlation ID, and a minimized/masked source-network signal only when justified. IP addresses and stable actor identifiers can be personal data: restrict access, define retention, and hash or truncate where incident-response needs permit. Silent auth failures are unacceptable, but audit metadata is not exempt from privacy policy. Make 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 uses the canonical package manager's frozen/immutable install mode, with lifecycle scripts disabled where the build allows. Never translate the command to a different package manager.
- 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?
- Semantic text normalized before its minimum-length check, with a whitespace-only negative test (unless preserving whitespace is intentional)?
- 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: avoided where possible; otherwise host allow-listed, every resolved/redirect IP screened, connection pinned without breaking TLS SNI/Host validation, and implemented through a reviewed client/egress layer (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?
What ships with it: 2 files
3.4 KB alongside SKILL.md
agents/
- openai.yaml200 B
references/
- localized-validation.md3.2 KB