Fastify Security Audit
Audit Fastify HTTP servers. Fastify's schema-first design provides strong defaults if used correctly.
When this skill applies
- Reviewing Fastify route definitions and schemas
- Auditing plugin chain and encapsulation
- Reviewing hooks (onRequest, preHandler, preValidation, onResponse)
- Checking security plugin configuration
Workflow
Follow ../_shared/audit-workflow.md. Companion: nodejs-express-security for cross-cutting Node concerns.
Phase 1: Stack detection
grep -E '"fastify":|"@fastify/' package.json
Phase 2: Inventory
# Route definitions
grep -rn 'fastify\.\(get\|post\|put\|delete\|patch\|register\)' src/ | head -50
# Schemas
grep -rnE 'schema:\s*{' src/ | head -20
# Hooks
grep -rn '\.addHook\(\|preHandler:\|preValidation:\|onRequest:' src/
# Security plugins
grep -nE '@fastify/(helmet|cors|rate-limit|jwt|cookie|session|multipart|csrf-protection)' package.json
Phase 3: Detection — the checks
Schema validation
Fastify validates inputs against JSON Schema on every request — if you provide one.
- FST-SCH-1 Every route has a schema for
body, params, querystring. Missing schema = no validation.
- FST-SCH-2 Schema uses strict types and ranges:
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['email', 'password'],
additionalProperties: false, // ← strips/rejects extras
properties: {
email: { type: 'string', format: 'email', maxLength: 254 },
password: { type: 'string', minLength: 8, maxLength: 128 },
},
},
},
}, async (req, reply) => { ... });
- FST-SCH-3
additionalProperties: false set globally OR on every schema. Without it, mass assignment is possible.
- FST-SCH-4 Response schemas defined — they filter the response to only declared fields (built-in defense against accidental data exposure):
schema: {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
displayName: { type: 'string' },
// passwordHash explicitly absent
},
},
},
}
- FST-SCH-5 AJV configured strictly (default in current Fastify); custom keywords reviewed for soundness.
Hooks
Fastify has multiple hook points; auth typically in onRequest or preHandler.
- FST-HK-1 Auth hook applied to protected routes via plugin scope or global hook with opt-out for public routes.
- FST-HK-2 Hooks run in registration order; auth before validation is fine, but verify the order matches intent.
- FST-HK-3 Hooks throwing errors propagate to error handler — don't swallow.
Plugin encapsulation
Fastify plugins create scopes. Auth applied in one plugin doesn't apply to sibling plugins unless registered up-stack:
// BAD — auth only applies to /api/v1 subtree
fastify.register(async (app) => {
app.addHook('preHandler', authHook);
app.register(userRoutes, { prefix: '/api/v1' });
});
// /api/v2 routes registered elsewhere have NO auth
// GOOD — auth at top level
fastify.addHook('preHandler', authHook);
fastify.register(userRoutes, { prefix: '/api/v1' });
fastify.register(adminRoutes, { prefix: '/api/v2' });
- FST-PLG-1 Auth/security hooks at the top level OR encapsulated explicitly per plugin scope.
- FST-PLG-2
fastify-plugin wrapper used when a plugin's effects (including hooks) should escape its scope. Conversely, plugins that should be scoped should NOT use fastify-plugin.
Security plugins
- FST-SP-1
@fastify/helmet registered. Same options as Express helmet.
- FST-SP-2
@fastify/cors with specific origin allowlist.
- FST-SP-3
@fastify/rate-limit registered globally (or per-route for fine-tuning).
- FST-SP-4
@fastify/csrf-protection if using cookie-based session.
- FST-SP-5
@fastify/multipart with size limits if file uploads.
- FST-SP-6
@fastify/cookie and @fastify/session configured securely (httpOnly, secure, sameSite — see saas-security-pack/saas-frontend-hardening/references/cookie-config.md).
Body parser limits
- FST-BP-1
bodyLimit set on FastifyInstance (default 1MB; lower if appropriate, never higher without specific reason).
- FST-BP-2 Per-route override for upload routes only.
JWT (@fastify/jwt)
- FST-JWT-1 Secret/key from env, not committed.
- FST-JWT-2 Algorithm specified (don't accept
none).
- FST-JWT-3 See
saas-security-pack/saas-code-security-review/references/jwt-validation.md.
Error handling
- FST-ERR-1
setErrorHandler configured to scrub internal details in production.
- FST-ERR-2 Validation errors return generic messages (don't echo full schema paths that reveal internal field names).
Logging
- FST-LOG-1 Fastify's pino logger configured to redact sensitive paths:
const fastify = Fastify({
logger: {
redact: ['req.headers.authorization', 'req.headers.cookie', 'req.body.password'],
},
});
- FST-LOG-2 No password / token in request bodies logged at info level.
Microservice / WS
If using @fastify/websocket:
- FST-WS-1 WebSocket connection auth via the initial HTTP upgrade — same auth as REST routes.
- FST-WS-2 Origin validation on upgrade.
Dependencies
- FST-DEP-1 Fastify v4 or v5 (current). Older versions deprecated.
- FST-DEP-2
@fastify/* packages match Fastify major version.
Phase 4: Triage
Critical: route without schema accepting body; encapsulation bug where auth hook missing from a route group; default bodyLimit with file upload routes.
Phase 5: Report
Use ../_shared/findings-schema.md. Prefix IDs with FST-.
1---2name: fastify-security3description: Security audit for Fastify applications including schema validation, hooks (onRequest, preHandler, preValidation), plugin scoping, encapsulation, fastify-helmet/fastify-cors/fastify-rate-limit setup, JSON schema strictness, and Fastify-specific patterns. Use this skill whenever the user mentions Fastify, @fastify/*, fastify-plugin, FastifyInstance, route schemas, fastify hooks, or asks "audit my Fastify app", "Fastify security", "schema validation". Trigger when the codebase contains `fastify` or `@fastify/*` in package.json.4---56# Fastify Security Audit78Audit Fastify HTTP servers. Fastify's schema-first design provides strong defaults if used correctly.910## When this skill applies1112- Reviewing Fastify route definitions and schemas13- Auditing plugin chain and encapsulation14- Reviewing hooks (onRequest, preHandler, preValidation, onResponse)15- Checking security plugin configuration1617## Workflow1819Follow `../_shared/audit-workflow.md`. Companion: `nodejs-express-security` for cross-cutting Node concerns.2021### Phase 1: Stack detection2223```bash24grep -E '"fastify":|"@fastify/' package.json25```2627### Phase 2: Inventory2829```bash30# Route definitions31grep -rn 'fastify\.\(get\|post\|put\|delete\|patch\|register\)' src/ | head -503233# Schemas34grep -rnE 'schema:\s*{' src/ | head -203536# Hooks37grep -rn '\.addHook\(\|preHandler:\|preValidation:\|onRequest:' src/3839# Security plugins40grep -nE '@fastify/(helmet|cors|rate-limit|jwt|cookie|session|multipart|csrf-protection)' package.json41```4243### Phase 3: Detection — the checks4445#### Schema validation4647Fastify validates inputs against JSON Schema on every request — if you provide one.4849- **FST-SCH-1** Every route has a schema for `body`, `params`, `querystring`. Missing schema = no validation.50- **FST-SCH-2** Schema uses strict types and ranges:51 ```ts52 fastify.post('/users', {53 schema: {54 body: {55 type: 'object',56 required: ['email', 'password'],57 additionalProperties: false, // ← strips/rejects extras58 properties: {59 email: { type: 'string', format: 'email', maxLength: 254 },60 password: { type: 'string', minLength: 8, maxLength: 128 },61 },62 },63 },64 }, async (req, reply) => { ... });65 ```66- **FST-SCH-3** `additionalProperties: false` set globally OR on every schema. Without it, mass assignment is possible.67- **FST-SCH-4** Response schemas defined — they filter the response to only declared fields (built-in defense against accidental data exposure):68 ```ts69 schema: {70 response: {71 200: {72 type: 'object',73 properties: {74 id: { type: 'string' },75 displayName: { type: 'string' },76 // passwordHash explicitly absent77 },78 },79 },80 }81 ```82- **FST-SCH-5** AJV configured strictly (default in current Fastify); custom keywords reviewed for soundness.8384#### Hooks8586Fastify has multiple hook points; auth typically in `onRequest` or `preHandler`.8788- **FST-HK-1** Auth hook applied to protected routes via plugin scope or global hook with opt-out for public routes.89- **FST-HK-2** Hooks run in registration order; auth before validation is fine, but verify the order matches intent.90- **FST-HK-3** Hooks throwing errors propagate to error handler — don't swallow.9192#### Plugin encapsulation9394Fastify plugins create scopes. Auth applied in one plugin doesn't apply to sibling plugins unless registered up-stack:9596```ts97// BAD — auth only applies to /api/v1 subtree98fastify.register(async (app) => {99 app.addHook('preHandler', authHook);100 app.register(userRoutes, { prefix: '/api/v1' });101});102// /api/v2 routes registered elsewhere have NO auth103104// GOOD — auth at top level105fastify.addHook('preHandler', authHook);106fastify.register(userRoutes, { prefix: '/api/v1' });107fastify.register(adminRoutes, { prefix: '/api/v2' });108```109110- **FST-PLG-1** Auth/security hooks at the top level OR encapsulated explicitly per plugin scope.111- **FST-PLG-2** `fastify-plugin` wrapper used when a plugin's effects (including hooks) should escape its scope. Conversely, plugins that should be scoped should NOT use `fastify-plugin`.112113#### Security plugins114115- **FST-SP-1** `@fastify/helmet` registered. Same options as Express helmet.116- **FST-SP-2** `@fastify/cors` with specific origin allowlist.117- **FST-SP-3** `@fastify/rate-limit` registered globally (or per-route for fine-tuning).118- **FST-SP-4** `@fastify/csrf-protection` if using cookie-based session.119- **FST-SP-5** `@fastify/multipart` with size limits if file uploads.120- **FST-SP-6** `@fastify/cookie` and `@fastify/session` configured securely (httpOnly, secure, sameSite — see `saas-security-pack/saas-frontend-hardening/references/cookie-config.md`).121122#### Body parser limits123124- **FST-BP-1** `bodyLimit` set on FastifyInstance (default 1MB; lower if appropriate, never higher without specific reason).125- **FST-BP-2** Per-route override for upload routes only.126127#### JWT (`@fastify/jwt`)128129- **FST-JWT-1** Secret/key from env, not committed.130- **FST-JWT-2** Algorithm specified (don't accept `none`).131- **FST-JWT-3** See `saas-security-pack/saas-code-security-review/references/jwt-validation.md`.132133#### Error handling134135- **FST-ERR-1** `setErrorHandler` configured to scrub internal details in production.136- **FST-ERR-2** Validation errors return generic messages (don't echo full schema paths that reveal internal field names).137138#### Logging139140- **FST-LOG-1** Fastify's pino logger configured to redact sensitive paths:141 ```ts142 const fastify = Fastify({143 logger: {144 redact: ['req.headers.authorization', 'req.headers.cookie', 'req.body.password'],145 },146 });147 ```148- **FST-LOG-2** No password / token in request bodies logged at info level.149150#### Microservice / WS151152If using `@fastify/websocket`:153- **FST-WS-1** WebSocket connection auth via the initial HTTP upgrade — same auth as REST routes.154- **FST-WS-2** Origin validation on upgrade.155156#### Dependencies157158- **FST-DEP-1** Fastify v4 or v5 (current). Older versions deprecated.159- **FST-DEP-2** `@fastify/*` packages match Fastify major version.160161### Phase 4: Triage162163Critical: route without schema accepting body; encapsulation bug where auth hook missing from a route group; default `bodyLimit` with file upload routes.164165### Phase 5: Report166167Use `../_shared/findings-schema.md`. Prefix IDs with `FST-`.