start-vibing-stacks 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +16 -2
- package/dist/migrate.d.ts +27 -0
- package/dist/migrate.js +217 -0
- package/dist/setup.js +10 -0
- package/package.json +1 -1
- package/stacks/_shared/agents/claude-md-compactor.md +1 -0
- package/stacks/_shared/agents/commit-manager.md +1 -0
- package/stacks/_shared/agents/documenter.md +1 -0
- package/stacks/_shared/agents/domain-updater.md +1 -0
- package/stacks/_shared/agents/research-web.md +1 -0
- package/stacks/_shared/agents/security-auditor.md +168 -0
- package/stacks/_shared/agents/tester.md +1 -0
- package/stacks/_shared/hooks/final-check.ts +205 -0
- package/stacks/_shared/hooks/stop-validator.ts +77 -1
- package/stacks/_shared/skills/accessibility-wcag22/SKILL.md +284 -0
- package/stacks/_shared/skills/ci-pipelines/SKILL.md +166 -0
- package/stacks/_shared/skills/codebase-knowledge/SKILL.md +5 -0
- package/stacks/_shared/skills/database-migrations/SKILL.md +256 -0
- package/stacks/_shared/skills/debugging-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docker-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/docs-tracker/SKILL.md +5 -0
- package/stacks/_shared/skills/error-handling/SKILL.md +335 -0
- package/stacks/_shared/skills/final-check/SKILL.md +74 -37
- package/stacks/_shared/skills/git-workflow/SKILL.md +5 -0
- package/stacks/_shared/skills/hook-development/SKILL.md +5 -0
- package/stacks/_shared/skills/observability/SKILL.md +351 -0
- package/stacks/_shared/skills/performance-patterns/SKILL.md +5 -0
- package/stacks/_shared/skills/playwright-automation/SKILL.md +5 -0
- package/stacks/_shared/skills/quality-gate/SKILL.md +5 -0
- package/stacks/_shared/skills/research-cache/SKILL.md +5 -0
- package/stacks/_shared/skills/secrets-management/SKILL.md +245 -0
- package/stacks/_shared/skills/security-baseline/SKILL.md +202 -0
- package/stacks/_shared/skills/test-coverage/SKILL.md +5 -0
- package/stacks/_shared/skills/ui-ux-audit/SKILL.md +5 -0
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-standards/SKILL.md +5 -0
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/shadcn-ui/SKILL.md +5 -0
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +5 -0
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +5 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +5 -0
- package/stacks/nodejs/skills/api-security-node/SKILL.md +275 -0
- package/stacks/nodejs/skills/bun-runtime/SKILL.md +5 -0
- package/stacks/nodejs/skills/mongoose-patterns/SKILL.md +5 -0
- package/stacks/nodejs/skills/nextjs-app-router/SKILL.md +5 -0
- package/stacks/nodejs/skills/trpc-api/SKILL.md +5 -0
- package/stacks/nodejs/skills/typescript-strict/SKILL.md +5 -0
- package/stacks/nodejs/stack.json +2 -1
- package/stacks/nodejs/workflows/ci.yml +90 -0
- package/stacks/nodejs/workflows/security.yml +45 -0
- package/stacks/php/skills/api-design/SKILL.md +5 -0
- package/stacks/php/skills/api-security/SKILL.md +5 -0
- package/stacks/php/skills/composer-workflow/SKILL.md +5 -0
- package/stacks/php/skills/external-api-patterns/SKILL.md +5 -0
- package/stacks/php/skills/inertia-react/SKILL.md +5 -0
- package/stacks/php/skills/laravel-inertia-i18n/SKILL.md +5 -0
- package/stacks/php/skills/laravel-octane/SKILL.md +5 -0
- package/stacks/php/skills/laravel-patterns/SKILL.md +5 -0
- package/stacks/php/skills/mariadb-octane/SKILL.md +5 -0
- package/stacks/php/skills/php-patterns/SKILL.md +5 -0
- package/stacks/php/skills/phpstan-analysis/SKILL.md +5 -0
- package/stacks/php/skills/phpunit-testing/SKILL.md +5 -0
- package/stacks/php/skills/security-scan-php/SKILL.md +5 -0
- package/stacks/php/workflows/ci.yml +106 -0
- package/stacks/php/workflows/security.yml +36 -0
- package/stacks/python/skills/api-security-python/SKILL.md +312 -0
- package/stacks/python/skills/async-patterns/SKILL.md +5 -0
- package/stacks/python/skills/django-patterns/SKILL.md +5 -0
- package/stacks/python/skills/fastapi-patterns/SKILL.md +5 -0
- package/stacks/python/skills/pydantic-validation/SKILL.md +5 -0
- package/stacks/python/skills/pytest-testing/SKILL.md +5 -0
- package/stacks/python/skills/python-patterns/SKILL.md +5 -0
- package/stacks/python/skills/python-performance/SKILL.md +5 -0
- package/stacks/python/skills/scripting-automation/SKILL.md +5 -0
- package/stacks/python/stack.json +2 -1
- package/stacks/python/workflows/ci.yml +76 -0
- package/stacks/python/workflows/security.yml +56 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-baseline
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: Universal OWASP Top 10 baseline with stack-aware examples. Invoke before designing or reviewing any feature that touches user data, auth, persistence, or external IO.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Security Baseline — OWASP Top 10 (2021)
|
|
8
|
+
|
|
9
|
+
**ALWAYS invoke when designing auth, APIs, persistence, file uploads, or any user-input flow.**
|
|
10
|
+
|
|
11
|
+
> Threat model first. Then code. Defense in depth: every layer assumes the previous one failed.
|
|
12
|
+
|
|
13
|
+
## Core Principles
|
|
14
|
+
|
|
15
|
+
1. **Trust no input** — validate at every boundary (HTTP, queue, file, env, IPC).
|
|
16
|
+
2. **Least privilege** — minimum scopes, minimum table access, minimum filesystem rights.
|
|
17
|
+
3. **Fail closed** — on error, deny. Never default to "allow on exception".
|
|
18
|
+
4. **Defense in depth** — auth + authz + input validation + output encoding + audit log.
|
|
19
|
+
5. **No security by obscurity** — assume attacker has source code.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## A01 — Broken Access Control
|
|
24
|
+
|
|
25
|
+
| Anti-pattern | Fix |
|
|
26
|
+
|---|---|
|
|
27
|
+
| User ID from request body/query | Always derive from session/JWT |
|
|
28
|
+
| Unscoped `Model.findById(id)` | Scope by owner: `where userId == session.userId` |
|
|
29
|
+
| Role check in frontend only | Re-check on server |
|
|
30
|
+
| IDOR (sequential IDs leak existence) | Use UUIDs **and** authz check |
|
|
31
|
+
|
|
32
|
+
### Node.js (Next.js / Express)
|
|
33
|
+
```ts
|
|
34
|
+
// WRONG — uses body userId
|
|
35
|
+
const userId = req.body.userId;
|
|
36
|
+
const orders = await Order.find({ userId });
|
|
37
|
+
|
|
38
|
+
// CORRECT — derives from session
|
|
39
|
+
const session = await auth();
|
|
40
|
+
if (!session) throw new UnauthorizedError();
|
|
41
|
+
const orders = await Order.find({ userId: session.user.id });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Python (FastAPI)
|
|
45
|
+
```python
|
|
46
|
+
# CORRECT — Depends() resolves authenticated user
|
|
47
|
+
@app.get("/orders")
|
|
48
|
+
async def list_orders(user: User = Depends(current_user)):
|
|
49
|
+
return await Order.filter(user_id=user.id).all()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### PHP (Laravel)
|
|
53
|
+
```php
|
|
54
|
+
// CORRECT — Policy + scoped query
|
|
55
|
+
$orders = $request->user()->orders()->get();
|
|
56
|
+
$this->authorize('view', $order);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## A02 — Cryptographic Failures
|
|
62
|
+
|
|
63
|
+
- **Never** use MD5/SHA1 for passwords. Use bcrypt/argon2/scrypt.
|
|
64
|
+
- **TLS everywhere**. HSTS header in production.
|
|
65
|
+
- **Cookies**: `HttpOnly`, `Secure`, `SameSite=Strict|Lax`.
|
|
66
|
+
- **Secrets** never in code, never in logs, never in URLs.
|
|
67
|
+
- **JWT**: short expiry (15m access, 7d refresh), rotate refresh tokens, signed with strong secret.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// Node.js password hashing
|
|
71
|
+
import { hash, verify } from '@node-rs/argon2';
|
|
72
|
+
const hashed = await hash(password, { memoryCost: 19456, timeCost: 2 });
|
|
73
|
+
const ok = await verify(hashed, attempt);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## A03 — Injection
|
|
79
|
+
|
|
80
|
+
| Type | Fix |
|
|
81
|
+
|---|---|
|
|
82
|
+
| SQL | Parameterized queries / ORM with bindings |
|
|
83
|
+
| NoSQL | Sanitize operators (`$where`, `$regex`) — never accept raw object from user |
|
|
84
|
+
| Command | `execFile` with array args, never `exec` with string |
|
|
85
|
+
| LDAP/XPath/Template | Library-specific escapers |
|
|
86
|
+
| Header injection | Strip `\r\n` from any user-controlled header value |
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// WRONG — Mongo operator injection
|
|
90
|
+
User.findOne({ email: req.body.email });
|
|
91
|
+
// If body is { email: { $ne: null } } → returns first user
|
|
92
|
+
|
|
93
|
+
// CORRECT — coerce to string/Zod parse first
|
|
94
|
+
const { email } = z.object({ email: z.string().email() }).parse(req.body);
|
|
95
|
+
User.findOne({ email });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## A04 — Insecure Design
|
|
101
|
+
|
|
102
|
+
- Threat-model **before** coding new flows. Document threats in `domain.md`.
|
|
103
|
+
- Anti-automation: rate limit, CAPTCHA on signup/login/password reset.
|
|
104
|
+
- Business logic limits: max items per cart, max API calls per minute, max file size.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## A05 — Security Misconfiguration
|
|
109
|
+
|
|
110
|
+
- Disable directory listings, default accounts, sample apps.
|
|
111
|
+
- Set security headers: `Content-Security-Policy`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Permissions-Policy`.
|
|
112
|
+
- Error messages: generic to clients, detailed in server logs only.
|
|
113
|
+
- `NODE_ENV=production` / `APP_DEBUG=false` in prod — verify in CI.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## A06 — Vulnerable Components
|
|
118
|
+
|
|
119
|
+
- Run `npm audit` / `pip-audit` / `composer audit` in CI.
|
|
120
|
+
- Pin lockfiles. Use Dependabot/Renovate.
|
|
121
|
+
- See `supply-chain` notes in `secrets-management` skill.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## A07 — Authentication Failures
|
|
126
|
+
|
|
127
|
+
- Lockout after N failed attempts (with exponential backoff or CAPTCHA, **not** permanent — DoS risk).
|
|
128
|
+
- Require MFA for admin/sensitive actions.
|
|
129
|
+
- Rotate session token on login, logout, password change, privilege change.
|
|
130
|
+
- Password reset tokens: single-use, expire ≤ 1h, sent to email of record.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## A08 — Software & Data Integrity
|
|
135
|
+
|
|
136
|
+
- Verify signatures on dependencies where possible (Sigstore, npm provenance).
|
|
137
|
+
- Sign artifacts in CI/CD.
|
|
138
|
+
- Validate webhook signatures (Stripe, GitHub, etc.) **before** parsing body.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// Stripe webhook — verify FIRST
|
|
142
|
+
const sig = req.headers['stripe-signature'];
|
|
143
|
+
const event = stripe.webhooks.constructEvent(rawBody, sig, secret);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## A09 — Security Logging Failures
|
|
149
|
+
|
|
150
|
+
- Log: auth events, authz denials, validation failures, admin actions, payment events.
|
|
151
|
+
- Never log: passwords, tokens, full PAN, full SSN, raw cookies, secrets.
|
|
152
|
+
- Use correlation IDs to trace a request across services. See `observability` skill.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## A10 — Server-Side Request Forgery (SSRF)
|
|
157
|
+
|
|
158
|
+
- Allowlist outbound destinations when fetching user-supplied URLs.
|
|
159
|
+
- Block private IP ranges (10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7).
|
|
160
|
+
- Resolve DNS yourself and check the IP — defeats DNS rebinding.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { isIP } from 'net';
|
|
164
|
+
const PRIVATE = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.|169\.254\.|::1|fc|fd)/i;
|
|
165
|
+
const url = new URL(userUrl);
|
|
166
|
+
const ip = await dns.lookup(url.hostname);
|
|
167
|
+
if (PRIVATE.test(ip.address)) throw new ForbiddenError('Private IP not allowed');
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Mandatory Boundary Validation
|
|
173
|
+
|
|
174
|
+
Every external input MUST be validated by a schema:
|
|
175
|
+
|
|
176
|
+
| Stack | Library |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Node.js | Zod (`zod-validation` skill) |
|
|
179
|
+
| Python | Pydantic (`pydantic-validation` skill) |
|
|
180
|
+
| PHP | FormRequest + rules |
|
|
181
|
+
|
|
182
|
+
No exceptions for "trusted" sources. Internal services drift, queues replay malformed payloads, env files are edited by hand.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Pre-Commit Security Checklist
|
|
187
|
+
|
|
188
|
+
- [ ] All routes have authn + authz checks
|
|
189
|
+
- [ ] User ID derived from session, never from body
|
|
190
|
+
- [ ] All inputs validated by schema
|
|
191
|
+
- [ ] No secrets in code (see `secrets-management`)
|
|
192
|
+
- [ ] No raw SQL / Mongo operators from user input
|
|
193
|
+
- [ ] Security headers set on responses
|
|
194
|
+
- [ ] Rate limit on auth + write endpoints
|
|
195
|
+
- [ ] PII not in logs (see `observability`)
|
|
196
|
+
- [ ] Webhook signatures verified before parsing
|
|
197
|
+
|
|
198
|
+
## See Also
|
|
199
|
+
|
|
200
|
+
- `secrets-management` — env hygiene, gitleaks
|
|
201
|
+
- `observability` — log redaction
|
|
202
|
+
- `api-security-node` / `api-security-python` / PHP `api-security` — stack-specific hardening
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-security-node
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: Production-grade API hardening for Node.js (Express, Fastify, Next.js Route Handlers, Server Actions). Rate limit, CORS, JWT, secure cookies, CSRF, headers.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# API Security — Node.js
|
|
8
|
+
|
|
9
|
+
**ALWAYS invoke when building API endpoints, auth flows, Server Actions, or Route Handlers.**
|
|
10
|
+
|
|
11
|
+
> Pair this with `security-baseline` for OWASP Top 10. This skill is stack-specific hardening.
|
|
12
|
+
|
|
13
|
+
## Layered Defense
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Edge (CDN/WAF) → Rate Limit → CORS → Headers → Auth → Authz → Validate → Logic → Encode → Audit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Security Headers
|
|
22
|
+
|
|
23
|
+
### Express / Fastify
|
|
24
|
+
```ts
|
|
25
|
+
import helmet from 'helmet';
|
|
26
|
+
app.use(helmet({
|
|
27
|
+
contentSecurityPolicy: {
|
|
28
|
+
directives: {
|
|
29
|
+
defaultSrc: ["'self'"],
|
|
30
|
+
scriptSrc: ["'self'", "'strict-dynamic'", (_, res) => `'nonce-${res.locals.nonce}'`],
|
|
31
|
+
styleSrc: ["'self'", "'unsafe-inline'"], // Tailwind needs inline; otherwise remove
|
|
32
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
33
|
+
connectSrc: ["'self'"],
|
|
34
|
+
frameAncestors: ["'none'"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
|
|
38
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
39
|
+
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
40
|
+
}));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Next.js — `next.config.ts`
|
|
44
|
+
```ts
|
|
45
|
+
const securityHeaders = [
|
|
46
|
+
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
|
47
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
48
|
+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
|
49
|
+
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
|
50
|
+
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
51
|
+
];
|
|
52
|
+
export default {
|
|
53
|
+
async headers() { return [{ source: '/(.*)', headers: securityHeaders }]; },
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
CSP for Next.js is best done in middleware with per-request nonces (script-src `'strict-dynamic'`).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. CORS — Strict Allowlist
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import cors from 'cors';
|
|
65
|
+
const ALLOW = (process.env['CORS_ORIGINS'] ?? '').split(',').filter(Boolean);
|
|
66
|
+
app.use(cors({
|
|
67
|
+
origin: (origin, cb) => {
|
|
68
|
+
if (!origin) return cb(null, true); // server-to-server
|
|
69
|
+
if (ALLOW.includes(origin)) return cb(null, true);
|
|
70
|
+
return cb(new Error('CORS blocked'));
|
|
71
|
+
},
|
|
72
|
+
credentials: true, // required for cookies
|
|
73
|
+
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
|
|
74
|
+
maxAge: 600,
|
|
75
|
+
}));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Never** use `origin: '*'` with `credentials: true` — browsers will reject and you'll silently break auth.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 3. Rate Limiting
|
|
83
|
+
|
|
84
|
+
### Express — `express-rate-limit` + Redis
|
|
85
|
+
```ts
|
|
86
|
+
import rateLimit from 'express-rate-limit';
|
|
87
|
+
import RedisStore from 'rate-limit-redis';
|
|
88
|
+
|
|
89
|
+
const authLimiter = rateLimit({
|
|
90
|
+
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
|
|
91
|
+
windowMs: 15 * 60 * 1000,
|
|
92
|
+
max: 5, // 5 attempts / 15 min / IP
|
|
93
|
+
standardHeaders: true,
|
|
94
|
+
legacyHeaders: false,
|
|
95
|
+
skipSuccessfulRequests: true, // only count failures on /login
|
|
96
|
+
});
|
|
97
|
+
app.post('/auth/login', authLimiter, loginHandler);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Next.js — `@upstash/ratelimit`
|
|
101
|
+
```ts
|
|
102
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
103
|
+
import { Redis } from '@upstash/redis';
|
|
104
|
+
|
|
105
|
+
const limiter = new Ratelimit({
|
|
106
|
+
redis: Redis.fromEnv(),
|
|
107
|
+
limiter: Ratelimit.slidingWindow(10, '60s'),
|
|
108
|
+
analytics: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export async function POST(req: Request) {
|
|
112
|
+
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
|
|
113
|
+
const { success } = await limiter.limit(ip);
|
|
114
|
+
if (!success) return new Response('Too Many Requests', { status: 429 });
|
|
115
|
+
// ...
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Limits to set:** auth (5/15min), password reset (3/hour), signup (3/hour/IP), generic write (60/min/user).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 4. Cookies
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
res.cookie('session', token, {
|
|
127
|
+
httpOnly: true, // JS cannot read
|
|
128
|
+
secure: process.env['NODE_ENV'] === 'production',
|
|
129
|
+
sameSite: 'lax', // 'strict' if no cross-site flows
|
|
130
|
+
path: '/',
|
|
131
|
+
maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
|
|
132
|
+
domain: process.env['COOKIE_DOMAIN'], // e.g. .example.com
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For sensitive ops, also set a CSRF token cookie (readable) + require it in `X-CSRF-Token` header.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 5. JWT / Session Tokens
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { SignJWT, jwtVerify } from 'jose';
|
|
144
|
+
|
|
145
|
+
const SECRET = new TextEncoder().encode(process.env['JWT_SECRET']);
|
|
146
|
+
|
|
147
|
+
// Issue access token (short-lived) + refresh token (rotated)
|
|
148
|
+
const accessToken = await new SignJWT({ sub: user.id, role: user.role })
|
|
149
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
150
|
+
.setIssuedAt()
|
|
151
|
+
.setExpirationTime('15m')
|
|
152
|
+
.setJti(crypto.randomUUID())
|
|
153
|
+
.sign(SECRET);
|
|
154
|
+
|
|
155
|
+
// Verify
|
|
156
|
+
const { payload } = await jwtVerify(token, SECRET, {
|
|
157
|
+
algorithms: ['HS256'], // pin algorithm — never accept 'none'
|
|
158
|
+
clockTolerance: 5,
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Rules:
|
|
163
|
+
- Access tokens: ≤ 15 min. Refresh tokens: rotate on use, store hash in DB, revocable.
|
|
164
|
+
- Pin algorithm. The `alg: 'none'` and key-confusion attacks are real.
|
|
165
|
+
- Include `jti` for revocation lists.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 6. CSRF — Next.js Server Actions / Route Handlers
|
|
170
|
+
|
|
171
|
+
Server Actions: Next.js 14+ verifies origin automatically when called via form/`useFormState`. For Route Handlers and any cross-origin form, implement double-submit cookie:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// middleware.ts
|
|
175
|
+
import { NextResponse } from 'next/server';
|
|
176
|
+
import type { NextRequest } from 'next/server';
|
|
177
|
+
|
|
178
|
+
export function middleware(req: NextRequest) {
|
|
179
|
+
const res = NextResponse.next();
|
|
180
|
+
if (!req.cookies.get('csrf-token')) {
|
|
181
|
+
res.cookies.set('csrf-token', crypto.randomUUID(), {
|
|
182
|
+
sameSite: 'lax', secure: true, path: '/',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
186
|
+
const cookie = req.cookies.get('csrf-token')?.value;
|
|
187
|
+
const header = req.headers.get('x-csrf-token');
|
|
188
|
+
if (!cookie || cookie !== header) {
|
|
189
|
+
return new NextResponse('CSRF', { status: 403 });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return res;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 7. Input Validation Boundary (Zod)
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { z } from 'zod';
|
|
202
|
+
|
|
203
|
+
const Body = z.object({
|
|
204
|
+
email: z.string().email().max(254),
|
|
205
|
+
age: z.number().int().min(13).max(120),
|
|
206
|
+
}).strict(); // .strict() rejects unknown keys → blocks mass assignment
|
|
207
|
+
|
|
208
|
+
export async function POST(req: Request) {
|
|
209
|
+
const parsed = Body.safeParse(await req.json());
|
|
210
|
+
if (!parsed.success) {
|
|
211
|
+
return Response.json({ errors: parsed.error.flatten() }, { status: 422 });
|
|
212
|
+
}
|
|
213
|
+
// parsed.data is type-safe and clean
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 8. File Upload
|
|
220
|
+
|
|
221
|
+
- Cap size at the proxy AND in the handler.
|
|
222
|
+
- Validate MIME by **magic bytes** (`file-type` package), not by extension or `Content-Type`.
|
|
223
|
+
- Store outside webroot. Serve via signed URLs.
|
|
224
|
+
- Never use the user-supplied filename on disk — generate a UUID.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
228
|
+
const ft = await fileTypeFromBuffer(buf);
|
|
229
|
+
if (!ft || !['image/jpeg', 'image/png', 'image/webp'].includes(ft.mime)) {
|
|
230
|
+
throw new BadRequestError('Invalid file type');
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 9. Password Hashing
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { hash, verify } from '@node-rs/argon2';
|
|
240
|
+
const hashed = await hash(password, { memoryCost: 19456, timeCost: 2, parallelism: 1 });
|
|
241
|
+
const ok = await verify(hashed, attempt); // constant-time
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Never use `bcryptjs` (pure JS, slow); prefer native `bcrypt` or `argon2`. Argon2id is the modern default.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 10. Server Action / Route Handler Checklist
|
|
249
|
+
|
|
250
|
+
- [ ] `auth()` called first; reject if no session for protected routes
|
|
251
|
+
- [ ] User ID from session, **never** from body
|
|
252
|
+
- [ ] Zod schema with `.strict()`
|
|
253
|
+
- [ ] Authz check (RBAC/ABAC) on the resource
|
|
254
|
+
- [ ] Rate limit applied
|
|
255
|
+
- [ ] No `console.log(req.body)` (PII leak — see `observability`)
|
|
256
|
+
- [ ] Errors return generic message; details to logs only
|
|
257
|
+
|
|
258
|
+
## FORBIDDEN
|
|
259
|
+
|
|
260
|
+
| Anti-pattern | Reason |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `cors({ origin: '*', credentials: true })` | Browsers reject; you're disabling auth |
|
|
263
|
+
| `jwt.verify(token)` without `algorithms` | `alg: none` and key confusion attacks |
|
|
264
|
+
| Storing JWT in `localStorage` | XSS exfiltration trivial — use HttpOnly cookies |
|
|
265
|
+
| `app.use(express.json({ limit: '50mb' }))` everywhere | DoS — set per-route |
|
|
266
|
+
| `req.body.userId` for ownership | A01 violation — use session |
|
|
267
|
+
| Logging `req.body` or `req.headers` | Logs passwords, cookies, tokens |
|
|
268
|
+
| `bcrypt.compare(a, b)` with non-string | Type coercion bugs leak info |
|
|
269
|
+
|
|
270
|
+
## See Also
|
|
271
|
+
|
|
272
|
+
- `security-baseline` — OWASP Top 10
|
|
273
|
+
- `secrets-management` — env vars, rotation
|
|
274
|
+
- `zod-validation` — schema patterns
|
|
275
|
+
- `observability` — structured logs without PII
|