start-vibing-stacks 2.6.0 → 2.7.3
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/README.md +83 -135
- 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,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: secrets-management
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: Environment variable hygiene, secret detection, rotation, and secret-store patterns. Invoke whenever .env, secrets, API keys, or env-var-reading code are touched.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Secrets Management
|
|
8
|
+
|
|
9
|
+
**ALWAYS invoke when touching `.env`, `process.env`, `os.environ`, secrets, API keys, or auth credentials.**
|
|
10
|
+
|
|
11
|
+
## Core Rules
|
|
12
|
+
|
|
13
|
+
1. **No secrets in code, ever.** Not in tests, not in fixtures, not in comments.
|
|
14
|
+
2. **No secrets in logs.** Redact before logging.
|
|
15
|
+
3. **No secrets in URLs / query strings.** Headers or body only — URLs land in proxy logs.
|
|
16
|
+
4. **No secrets in error messages** returned to clients.
|
|
17
|
+
5. **`.env` is in `.gitignore`. `.env.example` is committed.** No exceptions.
|
|
18
|
+
6. **Rotate after any leak.** Even suspected. The risk window is the time it takes to rotate, not when you think the leak started.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## File Layout
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
.env # gitignored, real values, local dev
|
|
26
|
+
.env.example # committed, placeholders only
|
|
27
|
+
.env.production # NEVER committed; use platform secret store
|
|
28
|
+
.env.test # gitignored if real keys; committed if all-fake
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`.gitignore`:
|
|
32
|
+
```
|
|
33
|
+
.env
|
|
34
|
+
.env.*
|
|
35
|
+
!.env.example
|
|
36
|
+
!.env.test.example
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`.env.example` template:
|
|
40
|
+
```bash
|
|
41
|
+
# Application
|
|
42
|
+
NODE_ENV=development
|
|
43
|
+
APP_URL=http://localhost:3000
|
|
44
|
+
|
|
45
|
+
# Database
|
|
46
|
+
DATABASE_URL=postgres://user:pass@localhost:5432/dbname
|
|
47
|
+
|
|
48
|
+
# Auth
|
|
49
|
+
JWT_SECRET=<generate: openssl rand -base64 32>
|
|
50
|
+
SESSION_SECRET=<generate: openssl rand -base64 32>
|
|
51
|
+
|
|
52
|
+
# Third-party (use service prefix to scan with allowlists)
|
|
53
|
+
STRIPE_SECRET_KEY=sk_test_...
|
|
54
|
+
OPENAI_API_KEY=sk-...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Reading Env Vars Safely
|
|
60
|
+
|
|
61
|
+
### Node.js / TypeScript
|
|
62
|
+
```ts
|
|
63
|
+
// Validate at boot — fail fast if missing
|
|
64
|
+
import { z } from 'zod';
|
|
65
|
+
|
|
66
|
+
const Env = z.object({
|
|
67
|
+
NODE_ENV: z.enum(['development', 'test', 'production']),
|
|
68
|
+
DATABASE_URL: z.string().url(),
|
|
69
|
+
JWT_SECRET: z.string().min(32),
|
|
70
|
+
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const env = Env.parse(process.env);
|
|
74
|
+
// Now `env.JWT_SECRET` is typed and guaranteed present
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Bracket notation only (`tsconfig` strict):
|
|
78
|
+
```ts
|
|
79
|
+
process.env['JWT_SECRET'] // CORRECT
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Python
|
|
83
|
+
```python
|
|
84
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
85
|
+
|
|
86
|
+
class Settings(BaseSettings):
|
|
87
|
+
model_config = SettingsConfigDict(env_file=".env", extra="forbid")
|
|
88
|
+
database_url: str
|
|
89
|
+
jwt_secret: str
|
|
90
|
+
stripe_secret_key: str
|
|
91
|
+
|
|
92
|
+
settings = Settings() # raises if missing/invalid
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### PHP / Laravel
|
|
96
|
+
```php
|
|
97
|
+
// config/services.php — read once, cached via `php artisan config:cache`
|
|
98
|
+
return [
|
|
99
|
+
'stripe' => ['secret' => env('STRIPE_SECRET_KEY')],
|
|
100
|
+
];
|
|
101
|
+
// Use config('services.stripe.secret') in app code — NEVER env() at runtime
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Public vs Server-Only Vars
|
|
107
|
+
|
|
108
|
+
| Stack | Public prefix | Rule |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| Next.js | `NEXT_PUBLIC_` | Anything with this prefix is shipped to the browser |
|
|
111
|
+
| Vite | `VITE_` | Same — bundled into JS |
|
|
112
|
+
| CRA | `REACT_APP_` | Same |
|
|
113
|
+
|
|
114
|
+
`security-rules.json` enforces: nothing matching `*SECRET|*TOKEN|*PRIVATE|*PASSWORD|*CREDENTIAL` may have a public prefix.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Secret Stores (Production)
|
|
119
|
+
|
|
120
|
+
| Tool | When |
|
|
121
|
+
|---|---|
|
|
122
|
+
| **Vercel/Netlify env vars** | Simple deployments |
|
|
123
|
+
| **AWS Secrets Manager** / **GCP Secret Manager** / **Azure Key Vault** | Cloud-native, IAM-scoped |
|
|
124
|
+
| **Doppler** | Multi-environment sync |
|
|
125
|
+
| **HashiCorp Vault** | On-prem / self-hosted, dynamic creds |
|
|
126
|
+
| **SOPS + age** | Git-encrypted secrets (GitOps) |
|
|
127
|
+
| **1Password Connect / op-cli** | Team-shared dev secrets |
|
|
128
|
+
|
|
129
|
+
**Rule:** `.env.production` is **never** committed. Production secrets live in the platform store and are injected at deploy/runtime.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Detection — Block Before Commit
|
|
134
|
+
|
|
135
|
+
### gitleaks (recommended)
|
|
136
|
+
|
|
137
|
+
Install:
|
|
138
|
+
```bash
|
|
139
|
+
brew install gitleaks # macOS
|
|
140
|
+
# or: docker run --rm -v $(pwd):/repo zricethezav/gitleaks:latest detect -s /repo
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Pre-commit hook:
|
|
144
|
+
```bash
|
|
145
|
+
gitleaks protect --staged --redact --verbose
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
CI step:
|
|
149
|
+
```yaml
|
|
150
|
+
- name: Gitleaks scan
|
|
151
|
+
uses: gitleaks/gitleaks-action@v2
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Quick grep (as a fallback)
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Block obvious patterns in staged diff
|
|
158
|
+
git diff --cached -U0 | grep -nEi \
|
|
159
|
+
'(api[_-]?key|secret|token|bearer|password|aws_(access|secret)|private_key)\s*[:=]\s*["'\''][a-zA-Z0-9/+=_-]{16,}'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Rotation Playbook
|
|
165
|
+
|
|
166
|
+
When a secret leaks (or is suspected):
|
|
167
|
+
|
|
168
|
+
1. **Revoke** at the provider immediately. Do not wait for git history rewrites.
|
|
169
|
+
2. **Issue new secret**, deploy with new value.
|
|
170
|
+
3. **Update audit log** — what leaked, when, who, scope of access.
|
|
171
|
+
4. **Scan history**: `gitleaks detect --log-opts="--all"` — find every commit/branch that contained it.
|
|
172
|
+
5. **History rewrite is optional** — git filter-repo / BFG only if you also rotate. The leaked secret in history is already gone from the world's POV once rotated.
|
|
173
|
+
6. **Notify** — if customer data was reachable with the leaked secret, follow incident-disclosure policy.
|
|
174
|
+
|
|
175
|
+
Token lifetimes (default targets):
|
|
176
|
+
- Access token: ≤ 15 min
|
|
177
|
+
- Refresh token: ≤ 7 days, rotated on use
|
|
178
|
+
- Service-to-service token: ≤ 90 days, rotated automatically
|
|
179
|
+
- Long-lived (e.g. cron API keys): document expiry, calendar reminder
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Logging Without Leaks
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Redact known fields before logging
|
|
187
|
+
const REDACT = ['password', 'token', 'authorization', 'cookie', 'secret', 'apiKey', 'creditCard'];
|
|
188
|
+
|
|
189
|
+
function safeLog(obj: unknown) {
|
|
190
|
+
return JSON.parse(JSON.stringify(obj, (key, val) =>
|
|
191
|
+
REDACT.some(r => key.toLowerCase().includes(r.toLowerCase())) ? '[REDACTED]' : val
|
|
192
|
+
));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
logger.info({ event: 'login_attempt', body: safeLog(req.body) });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use `pino-noir`, `winston`'s `format.printf` filter, or your APM's redaction. Same for Python: `structlog` processors; PHP: Monolog `RedactProcessor`.
|
|
199
|
+
|
|
200
|
+
See `observability` skill for full structured-logging setup.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Supply-Chain Hygiene
|
|
205
|
+
|
|
206
|
+
| Stack | Audit |
|
|
207
|
+
|---|---|
|
|
208
|
+
| Node.js | `npm audit --audit-level=high`, `bun audit`, `pnpm audit` |
|
|
209
|
+
| Python | `pip-audit`, `safety check` |
|
|
210
|
+
| PHP | `composer audit` |
|
|
211
|
+
|
|
212
|
+
Lockfile rules:
|
|
213
|
+
- **Always commit** `package-lock.json` / `bun.lock` / `pnpm-lock.yaml` / `poetry.lock` / `uv.lock` / `composer.lock`.
|
|
214
|
+
- **Renovate** or **Dependabot** for automated PRs.
|
|
215
|
+
- Pin major versions; allow minor/patch auto-merge after CI passes.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## FORBIDDEN
|
|
220
|
+
|
|
221
|
+
| Pattern | Reason |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `.env` committed | Trivial leak |
|
|
224
|
+
| Secret in `README.md` / docstring | Leaks via search engines |
|
|
225
|
+
| Secret in test fixtures | Leaks via PR diffs / forks |
|
|
226
|
+
| Secret in `console.log` / `print` / `Log::info` | Ends up in CloudWatch / Datadog forever |
|
|
227
|
+
| Secret in URL query string | Logged by every proxy in the chain |
|
|
228
|
+
| Secret in commit message | Cannot delete, history rewrites costly |
|
|
229
|
+
| `process.env.SECRET ?? "fallback-value"` with real fallback | Hardcoded backup secret |
|
|
230
|
+
| Sharing via Slack/email | Use 1Password / Vault sharing |
|
|
231
|
+
|
|
232
|
+
## Pre-Commit Checklist
|
|
233
|
+
|
|
234
|
+
- [ ] `.env` in `.gitignore`
|
|
235
|
+
- [ ] `.env.example` updated when new var added
|
|
236
|
+
- [ ] gitleaks (or fallback grep) clean
|
|
237
|
+
- [ ] No secret in code, tests, fixtures, comments, logs
|
|
238
|
+
- [ ] No `NEXT_PUBLIC_*SECRET|*TOKEN|*PRIVATE` patterns
|
|
239
|
+
- [ ] Env vars validated at boot (Zod / Pydantic / config())
|
|
240
|
+
|
|
241
|
+
## See Also
|
|
242
|
+
|
|
243
|
+
- `security-baseline` — broader OWASP scope
|
|
244
|
+
- `observability` — log redaction details
|
|
245
|
+
- Stack `api-security-*` — usage patterns
|
|
@@ -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
|