astro-security — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited astro-security (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill is the Astro-specific layer on top of secure-coding. Astro mixes static-site generation, server-side rendering, and partial hydration in one codebase, which means the attack surface differs per page even within the same project. That mixed-mode reality is the single most underappreciated source of Astro security mistakes.
Triggers on:
astro.config.mjs/.ts, src/pages/, src/middleware.ts, src/content/ directories, @astrojs/* adapters, or decap-cms/netlify-cms admin-UI files.set:html, Astro.locals, middleware, content-collection schemas, MDX components, or adapter config.astro GitHub security tab).security-review or api-security when Astro is in the stack.secure-coding.api-security. Here for the Astro-specific implementation of API routes.p/javascript) → sast-orchestrator.package.json → cve-triage.iac-security where relevant..env, .env.local, or platform env-var stores → secrets-scanner.Six phases. Phase 1 (render-mode classification) is the foundation — without it you cannot reason about which security rules apply to a given page.
Astro has three output modes, set in astro.config.mjs:
[verify]-marked CVEs in dependencies) — not the running site. Once deployed, pages are plain HTML.export const prerender = false. Or default to SSR with export const prerender = true for pre-rendered routes.Reviewer rule: for every page or route, identify its render mode FIRST. The prerender flag, the absence/presence of an adapter, and any per-page exports decide which security rules apply. A page that "looks" dynamic in the source can still be SSG (so dynamic data is frozen at build time, possibly leaked to the static HTML); a page that "looks" static can be SSR (so input-validation and auth become live concerns).
Common mistake: assuming hybrid means "the safe parts are static". The pages that use Astro.request or read cookies must be SSR. If they slip into SSG by default, they may bake build-time-only data (env vars, request-context defaults) into the static output and leak it.
Astro auto-escapes content rendered via {expression} in templates. XSS arises where you opt out.
dangerouslySetInnerHTML. Renders raw HTML. <div set:html={userContent} /> <!-- XSS if userContent is user input -->
<Fragment set:html={trustedHtml} /> <!-- same risk, no wrapping element -->Default rule: never on user-controlled input. If you need rich-text input, sanitize via DOMPurify (server-side) or an isomorphic sanitizer before assignment.
directives** (client:load, client:visible, client:idle, client:only`) ship JS to the browser. Review what is hydrated, especially client-side props that flow from server-side fetches — props are serialized into the page HTML and visible in the browser. Same trust-boundary discipline as the Next.js Server/Client Component boundary.rehype-raw-style plugin.is:inline directive bypasses processing. Combined with templated content this can become an injection vector.Two server-side primitives, both common attack-surface entry points.
API routes (src/pages/api/*.ts):
export async function POST({ request, locals }: APIContext) {
const session = locals.session; // set by middleware
if (!session) return new Response("unauth", { status: 401 });
const body = await request.json();
const parsed = schema.safeParse(body); // zod / valibot
if (!parsed.success) return new Response("bad request", { status: 400 });
// ... business logic with explicit ownership check
}request.json() or request.formData().api-security API1 BOLA discipline.Middleware (src/middleware.ts):
context.locals, redirect, or rewrite.Astro.locals.Content collections (src/content/<collection>/*.{md,mdx,json,yaml}) are typed content, validated via a config.ts schema. Two security considerations:
z.object({...}) schema validates frontmatter; the body content is still arbitrary markdown/MDX. Schema is a type guard, not an XSS guard.Author-trust model checklist:
src/content/? In a Decap CMS setup (phase 6), this maps to "who has OAuth-authenticated edit access to the repo".src/components/* or src/lib/*? If yes, the author-trust model extends to your entire src/ tree — not just the markdown directory.Astro is adapter-agnostic, but each adapter has different security ergonomics.
process.env. KV / R2 / D1 / Durable Objects are accessed via bindings on context.locals.runtime.env. Smaller request body limits (~100MB), constrained CPU time, no Node API.Env vars:
PUBLIC_* prefix exposes the variable to the client bundle. Anything with PUBLIC_ is essentially public — treat it that way.PUBLIC_ prefix.astro:env (Astro 5+, opt-in) provides a typed schema for env access. Use it where available; it catches accidental client/server mismatches at build time.PUBLIC_ in your code. Anything with KEY, SECRET, TOKEN, API, PASSWORD after PUBLIC_ is a finding.Security headers:
experimental.csp). Where available, prefer it over hand-rolled middleware-set CSP.unsafe-inline defeats most of the point. Use nonces or hashes; Astro's CSP integration supports nonce flow.Decap is the most common CMS pairing for Astro: git-based, content stored as files in the repo, edited via a JS admin UI. Specific security implications:
OAuth backend:
/api/decap-auth/* exchange. Netlify Identity (the historical default) was deprecated.repo for GitHub if private; public_repo if public; nothing more). Audit the OAuth app's scope on the provider side.Token storage and XSS impact:
localStorage after login. This is by design (the SPA needs it for git-API calls)./admin/), no unsafe-inline in the admin route, treat the admin UI as a trust boundary distinct from the public site. If your CSP on the public site needs to be relaxed for any reason, the admin UI's CSP must be tightened independently.Editorial workflow:
publish_mode: editorial_workflow puts content edits into a draft branch, with PR review before merge.main. For multi-author setups or anything customer-facing, editorial workflow is the right default.Authorization model:
main. Editorial-workflow PRs go to feature branches; CODEOWNERS gates merging to main.Media library:
Layer 1: render mode identified per page (SSG / SSR / hybrid)?, all set:html reviewed for user-input source?, MDX scope contained to trusted authors?, API routes have auth + input validation?, middleware not the only auth gate?, env vars audited for accidental PUBLIC_ prefix?, Decap OAuth scope minimized + admin-UI CSP tightened?
Layer 2: Astro version up to date (check astro GitHub security advisories, [verify] markers on any specific CVE claims), adapter version current, Decap CMS version current, claims about OAuth scope behavior verified against the actual GitHub/GitLab OAuth app config (not assumed).
Astro security review — <app>
Astro: <x.y.z> | Adapter: <node | cloudflare | vercel | netlify>
CMS: <Decap | other | none>
Render-mode map:
output: <static | server | hybrid>
Per-page prerender exports: <list of explicit overrides>
Pages with auth/sessions: <should be SSR; verify>
Output sanitization:
set:html occurrences: <N, locations, source-of-input per occurrence>
MDX collections: <which, author scope>
client:* directives: <which, hydrated props review>
Server endpoints:
src/pages/api/* count: N
Auth check per handler: <yes for all | gaps on ...>
Input validation: <zod/valibot/... | none>
Middleware:
src/middleware.ts present: <yes/no>
Auth in middleware only: <FINDING if yes>
Headers set in middleware: <list>
Content collections:
Collections defined: <list, MDX-allowed yes/no per collection>
Author-trust model: <internal-team | broader>
Adapter + env vars:
Adapter: <name + version>
PUBLIC_* vars: <list, review for accidental exposure>
astro:env in use: <yes/no>
Security headers: <CSP, HSTS, etc. — present, nonce-based>
Decap CMS (if applicable):
OAuth backend: <self-hosted worker | Netlify Identity (deprecated) | other>
OAuth scope: <minimum repo scope | over-permissive — FINDING>
GitHub App vs OAuth app: <App | classic OAuth app — FINDING if classic on private repo>
Editorial workflow: <enabled | disabled>
Admin UI CSP: <strict | inherits public CSP — FINDING if same as public>
Media backend: <git-stored | external>
Version check:
Astro security advisories: <within N days | check>
Decap CMS version: <date of last update>
cve-triage handoff: <N deps with vulns>
Findings (severity-sorted, follow security-review format)
Verification-loop: ...~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.