@kaademos/secure-sdlc 1.0.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/.claude/agents/ai-security-engineer.md +209 -0
- package/.claude/agents/appsec-engineer.md +131 -0
- package/.claude/agents/cloud-platform-engineer.md +119 -0
- package/.claude/agents/dev-lead.md +138 -0
- package/.claude/agents/grc-analyst.md +143 -0
- package/.claude/agents/product-manager.md +100 -0
- package/.claude/agents/release-manager.md +126 -0
- package/.claude/agents/security-champion.md +148 -0
- package/.cursor/rules/secure-sdlc.mdc +98 -0
- package/.github/workflows/secure-sdlc-gate.yml +325 -0
- package/CHANGELOG.md +49 -0
- package/CLAUDE.md +195 -0
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/cli/bin/secure-sdlc.js +95 -0
- package/cli/src/commands/gate.js +129 -0
- package/cli/src/commands/init.js +219 -0
- package/cli/src/commands/install-mcp.js +121 -0
- package/cli/src/commands/kickoff.js +261 -0
- package/cli/src/commands/paths.js +33 -0
- package/cli/src/commands/review.js +53 -0
- package/cli/src/commands/status.js +122 -0
- package/cli/src/utils/banner.js +43 -0
- package/cli/src/utils/package-root.js +23 -0
- package/cli/src/utils/phase-detect.js +107 -0
- package/cli/src/utils/stack-detect.js +138 -0
- package/docs/templates/compliance-attestation.md +159 -0
- package/docs/templates/infra-security-review.md +133 -0
- package/docs/templates/release-sign-off.md +119 -0
- package/docs/templates/risk-register.md +72 -0
- package/docs/templates/sast-findings.md +110 -0
- package/docs/templates/security-requirements.md +98 -0
- package/docs/templates/test-security-report.md +143 -0
- package/docs/templates/threat-model.md +129 -0
- package/hooks/install.sh +37 -0
- package/hooks/pre-commit +208 -0
- package/hooks/pre-push +127 -0
- package/mcp/README.md +116 -0
- package/mcp/package.json +23 -0
- package/mcp/src/server.js +638 -0
- package/package.json +67 -0
- package/stacks/django.md +216 -0
- package/stacks/express.md +229 -0
- package/stacks/fastapi.md +247 -0
- package/stacks/nextjs.md +198 -0
- package/stacks/nodejs.md +28 -0
- package/stacks/rails.md +247 -0
- package/warp-workflows/README.md +25 -0
- package/warp-workflows/feature-kickoff.yaml +49 -0
- package/warp-workflows/pr-security-review.yaml +47 -0
- package/warp-workflows/release-gate.yaml +44 -0
- package/warp-workflows/sdlc-status.yaml +48 -0
- package/warp-workflows/threat-model.yaml +56 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# FastAPI Security Profile
|
|
2
|
+
|
|
3
|
+
**Framework:** FastAPI
|
|
4
|
+
**Language:** Python 3.10+
|
|
5
|
+
**ASVS Baseline:** L2
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Critical Security Areas for FastAPI
|
|
10
|
+
|
|
11
|
+
### Authentication — Dependency Injection
|
|
12
|
+
|
|
13
|
+
FastAPI has **no global auth middleware by default**. Every endpoint must declare an auth dependency:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from fastapi import FastAPI, Depends, HTTPException, status
|
|
17
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
18
|
+
from jose import JWTError, jwt
|
|
19
|
+
|
|
20
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
21
|
+
|
|
22
|
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
23
|
+
credentials_exception = HTTPException(
|
|
24
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
25
|
+
detail="Could not validate credentials",
|
|
26
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
27
|
+
)
|
|
28
|
+
try:
|
|
29
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
|
30
|
+
user_id: str = payload.get("sub")
|
|
31
|
+
if user_id is None:
|
|
32
|
+
raise credentials_exception
|
|
33
|
+
except JWTError:
|
|
34
|
+
raise credentials_exception
|
|
35
|
+
|
|
36
|
+
user = await get_user(user_id)
|
|
37
|
+
if user is None:
|
|
38
|
+
raise credentials_exception
|
|
39
|
+
return user
|
|
40
|
+
|
|
41
|
+
# ✓ Every protected endpoint must declare the dependency
|
|
42
|
+
@app.get("/users/me")
|
|
43
|
+
async def read_users_me(current_user: User = Depends(get_current_user)):
|
|
44
|
+
return current_user
|
|
45
|
+
|
|
46
|
+
# ✗ Missing Depends — this is public even if you think it isn't
|
|
47
|
+
@app.get("/admin/users")
|
|
48
|
+
async def list_all_users():
|
|
49
|
+
return await db.users.find_many()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Object-Level Authorisation (IDOR Prevention)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# ✗ IDOR vulnerability — any authenticated user can get any post
|
|
56
|
+
@app.get("/posts/{post_id}")
|
|
57
|
+
async def get_post(post_id: int, current_user: User = Depends(get_current_user)):
|
|
58
|
+
post = await db.posts.find_unique(where={"id": post_id})
|
|
59
|
+
if not post:
|
|
60
|
+
raise HTTPException(status_code=404)
|
|
61
|
+
return post # ✗ Returns any user's post
|
|
62
|
+
|
|
63
|
+
# ✓ Check ownership
|
|
64
|
+
@app.get("/posts/{post_id}")
|
|
65
|
+
async def get_post(post_id: int, current_user: User = Depends(get_current_user)):
|
|
66
|
+
post = await db.posts.find_unique(where={"id": post_id})
|
|
67
|
+
if not post:
|
|
68
|
+
raise HTTPException(status_code=404)
|
|
69
|
+
if post.author_id != current_user.id:
|
|
70
|
+
raise HTTPException(status_code=403)
|
|
71
|
+
return post
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Input Validation with Pydantic
|
|
75
|
+
|
|
76
|
+
FastAPI's Pydantic integration is excellent — use it fully:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pydantic import BaseModel, Field, field_validator
|
|
80
|
+
import re
|
|
81
|
+
|
|
82
|
+
class CreateUserRequest(BaseModel):
|
|
83
|
+
username: str = Field(min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_]+$')
|
|
84
|
+
email: str = Field(max_length=255)
|
|
85
|
+
password: str = Field(min_length=8, max_length=128)
|
|
86
|
+
|
|
87
|
+
@field_validator('email')
|
|
88
|
+
@classmethod
|
|
89
|
+
def validate_email(cls, v: str) -> str:
|
|
90
|
+
# Use email-validator library for proper validation
|
|
91
|
+
from email_validator import validate_email, EmailNotValidError
|
|
92
|
+
try:
|
|
93
|
+
info = validate_email(v, check_deliverability=False)
|
|
94
|
+
return info.normalized
|
|
95
|
+
except EmailNotValidError:
|
|
96
|
+
raise ValueError('Invalid email address')
|
|
97
|
+
|
|
98
|
+
@field_validator('password')
|
|
99
|
+
@classmethod
|
|
100
|
+
def validate_password_strength(cls, v: str) -> str:
|
|
101
|
+
if not re.search(r'[A-Z]', v):
|
|
102
|
+
raise ValueError('Password must contain uppercase letter')
|
|
103
|
+
if not re.search(r'[0-9]', v):
|
|
104
|
+
raise ValueError('Password must contain a number')
|
|
105
|
+
return v
|
|
106
|
+
|
|
107
|
+
# Using response_model prevents accidental data leakage
|
|
108
|
+
class UserResponse(BaseModel):
|
|
109
|
+
id: int
|
|
110
|
+
username: str
|
|
111
|
+
email: str
|
|
112
|
+
# password_hash is NOT in the response model — it won't be returned
|
|
113
|
+
|
|
114
|
+
@app.post("/users", response_model=UserResponse)
|
|
115
|
+
async def create_user(request: CreateUserRequest):
|
|
116
|
+
...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Never use `response_model=None`** on endpoints that handle sensitive data. Pydantic response models
|
|
120
|
+
are your last-line-of-defence against accidentally returning internal fields.
|
|
121
|
+
|
|
122
|
+
### CORS — Never Use Wildcard for Authenticated APIs
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
126
|
+
|
|
127
|
+
# ✗ Wildcard — any origin can make authenticated requests
|
|
128
|
+
app.add_middleware(
|
|
129
|
+
CORSMiddleware,
|
|
130
|
+
allow_origins=["*"],
|
|
131
|
+
allow_credentials=True, # ✗ CRITICAL: credentials + wildcard is forbidden by spec but some browsers allow it
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ✓ Explicit origins
|
|
135
|
+
app.add_middleware(
|
|
136
|
+
CORSMiddleware,
|
|
137
|
+
allow_origins=[
|
|
138
|
+
"https://app.yourdomain.com",
|
|
139
|
+
"https://admin.yourdomain.com",
|
|
140
|
+
],
|
|
141
|
+
allow_credentials=True,
|
|
142
|
+
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
143
|
+
allow_headers=["Authorization", "Content-Type"],
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Error Handling — No Internal State Leakage
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from fastapi import Request
|
|
151
|
+
from fastapi.responses import JSONResponse
|
|
152
|
+
import logging
|
|
153
|
+
|
|
154
|
+
logger = logging.getLogger(__name__)
|
|
155
|
+
|
|
156
|
+
# ✓ Global exception handler — generic response to client, detailed log server-side
|
|
157
|
+
@app.exception_handler(Exception)
|
|
158
|
+
async def global_exception_handler(request: Request, exc: Exception):
|
|
159
|
+
logger.error(
|
|
160
|
+
"Unhandled exception",
|
|
161
|
+
exc_info=exc,
|
|
162
|
+
extra={
|
|
163
|
+
"path": request.url.path,
|
|
164
|
+
"method": request.method,
|
|
165
|
+
"user_id": getattr(request.state, "user_id", "anonymous"),
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
return JSONResponse(
|
|
169
|
+
status_code=500,
|
|
170
|
+
content={"detail": "An internal error occurred"} # No stack trace
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# ✗ Default exception behaviour in debug mode exposes internals
|
|
174
|
+
# NEVER set debug=True in production
|
|
175
|
+
app = FastAPI(debug=False)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Rate Limiting
|
|
179
|
+
|
|
180
|
+
FastAPI has no built-in rate limiting. Add it:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
184
|
+
from slowapi.util import get_remote_address
|
|
185
|
+
from slowapi.errors import RateLimitExceeded
|
|
186
|
+
|
|
187
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
188
|
+
app.state.limiter = limiter
|
|
189
|
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
190
|
+
|
|
191
|
+
@app.post("/auth/login")
|
|
192
|
+
@limiter.limit("5/minute") # 5 attempts per minute per IP
|
|
193
|
+
async def login(request: Request, credentials: LoginRequest):
|
|
194
|
+
...
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Secrets Management
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# ✗ Never do this
|
|
203
|
+
SECRET_KEY = "my-super-secret-key-that-is-in-git"
|
|
204
|
+
|
|
205
|
+
# ✓ Use pydantic-settings for typed, validated config
|
|
206
|
+
from pydantic_settings import BaseSettings
|
|
207
|
+
from pydantic import SecretStr
|
|
208
|
+
|
|
209
|
+
class Settings(BaseSettings):
|
|
210
|
+
secret_key: SecretStr # SecretStr prevents accidental logging
|
|
211
|
+
database_url: SecretStr
|
|
212
|
+
allowed_origins: list[str] = []
|
|
213
|
+
|
|
214
|
+
class Config:
|
|
215
|
+
env_file = ".env" # For local dev only — never commit .env
|
|
216
|
+
|
|
217
|
+
settings = Settings()
|
|
218
|
+
# Access: settings.secret_key.get_secret_value()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## ASVS Controls for FastAPI Projects
|
|
224
|
+
|
|
225
|
+
| ASVS Ref | Control | FastAPI Implementation |
|
|
226
|
+
|----------|---------|----------------------|
|
|
227
|
+
| V4.1.1 | Auth on all endpoints | `Depends(get_current_user)` on every protected endpoint |
|
|
228
|
+
| V4.2.1 | Object-level authorisation | `resource.owner_id == current_user.id` check |
|
|
229
|
+
| V5.1.3 | Input validation | Pydantic models with `Field` constraints |
|
|
230
|
+
| V8.3.4 | Don't confirm resource existence | Return 404 (not 403) for resources the user can't see |
|
|
231
|
+
| V13.2.5 | Rate limiting | slowapi or similar |
|
|
232
|
+
| V14.4.1 | Security headers | Add `SecurityHeadersMiddleware` |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Recommended Security Stack (2026)
|
|
237
|
+
|
|
238
|
+
| Category | Recommended |
|
|
239
|
+
|----------|-------------|
|
|
240
|
+
| Auth | python-jose + passlib, or Authlib |
|
|
241
|
+
| Input validation | Pydantic v2 (built-in) |
|
|
242
|
+
| Rate limiting | slowapi, fastapi-limiter (Redis-backed) |
|
|
243
|
+
| Security headers | secure (adds headers middleware) |
|
|
244
|
+
| Password hashing | passlib[bcrypt] or argon2-cffi |
|
|
245
|
+
| Secrets | pydantic-settings + AWS SM / Vault |
|
|
246
|
+
| ORM (injection-safe) | SQLAlchemy 2.0, Tortoise ORM |
|
|
247
|
+
| Secret scanning CI | gitleaks, detect-secrets |
|
package/stacks/nextjs.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Next.js Security Profile
|
|
2
|
+
|
|
3
|
+
**Framework:** Next.js (App Router + Pages Router)
|
|
4
|
+
**Language:** TypeScript / JavaScript
|
|
5
|
+
**ASVS Baseline:** L2
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Critical Security Areas for Next.js
|
|
10
|
+
|
|
11
|
+
### Server Actions
|
|
12
|
+
|
|
13
|
+
Server Actions are POST endpoints. They share the same attack surface as API routes:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// ✗ Missing auth check — any user can invoke this action
|
|
17
|
+
'use server'
|
|
18
|
+
export async function deletePost(postId: string) {
|
|
19
|
+
await db.posts.delete({ where: { id: postId } });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ✓ Correct — validate session and ownership
|
|
23
|
+
'use server'
|
|
24
|
+
export async function deletePost(postId: string) {
|
|
25
|
+
const session = await getServerSession(authOptions);
|
|
26
|
+
if (!session?.user?.id) throw new Error('Unauthorized');
|
|
27
|
+
|
|
28
|
+
const post = await db.posts.findUnique({ where: { id: postId } });
|
|
29
|
+
if (post?.authorId !== session.user.id) throw new Error('Forbidden');
|
|
30
|
+
|
|
31
|
+
await db.posts.delete({ where: { id: postId } });
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**CSRF on Server Actions:** Next.js 14+ includes CSRF protection for Server Actions by default
|
|
36
|
+
via `Origin` header validation. Do not disable this. If using custom fetch with `cache: 'force-cache'`,
|
|
37
|
+
be aware that CSRF protection may not apply.
|
|
38
|
+
|
|
39
|
+
### API Routes — App Router
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// ✗ No authentication
|
|
43
|
+
export async function GET(request: Request) {
|
|
44
|
+
const data = await db.users.findMany();
|
|
45
|
+
return Response.json(data);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✓ Authenticate every API route handler
|
|
49
|
+
export async function GET(request: Request) {
|
|
50
|
+
const session = await getServerSession(authOptions);
|
|
51
|
+
if (!session) return new Response('Unauthorized', { status: 401 });
|
|
52
|
+
|
|
53
|
+
// IDOR check: only return the requesting user's data
|
|
54
|
+
const data = await db.users.findUnique({ where: { id: session.user.id } });
|
|
55
|
+
return Response.json(data);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
There is **no middleware-level auth applied to all API routes by default** in Next.js.
|
|
60
|
+
Every route handler must explicitly authenticate.
|
|
61
|
+
|
|
62
|
+
### Middleware — Correct and Incorrect Use
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// next/middleware.ts
|
|
66
|
+
|
|
67
|
+
// ✓ Use middleware for: redirect to login, edge auth checks, rate limiting
|
|
68
|
+
export function middleware(request: NextRequest) {
|
|
69
|
+
const token = request.cookies.get('next-auth.session-token');
|
|
70
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
71
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ✗ Do NOT rely on middleware as your ONLY auth check — it runs at the edge
|
|
76
|
+
// and can be bypassed. Always validate auth in your route handlers too.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Server Components vs Client Components — Secret Leakage
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// ✗ CRITICAL: Secrets passed to Client Components are sent to the browser
|
|
83
|
+
async function Page() {
|
|
84
|
+
const apiKey = process.env.EXTERNAL_API_KEY; // This is fine as a server-side value
|
|
85
|
+
return <ClientComponent apiKey={apiKey} />; // ✗ Now it's in the browser
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ✓ Fetch server-side, pass only the result
|
|
89
|
+
async function Page() {
|
|
90
|
+
const data = await fetchWithApiKey(process.env.EXTERNAL_API_KEY);
|
|
91
|
+
return <ClientComponent data={data} />; // ✓ Only the result goes to client
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Rule:** `NEXT_PUBLIC_` prefix exposes variables to the browser. Never put secrets there.
|
|
96
|
+
|
|
97
|
+
### next.config.js — Security Headers
|
|
98
|
+
|
|
99
|
+
Add security headers. Without these, browsers have no CSP, HSTS, or clickjacking protection:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// next.config.js
|
|
103
|
+
const securityHeaders = [
|
|
104
|
+
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
|
|
105
|
+
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
|
106
|
+
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
|
|
107
|
+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
108
|
+
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
|
|
109
|
+
{
|
|
110
|
+
key: 'Content-Security-Policy',
|
|
111
|
+
value: [
|
|
112
|
+
"default-src 'self'",
|
|
113
|
+
"script-src 'self' 'nonce-{NONCE}'", // Use nonces for inline scripts
|
|
114
|
+
"style-src 'self' 'unsafe-inline'", // Tighten if possible
|
|
115
|
+
"img-src 'self' data: https:",
|
|
116
|
+
"font-src 'self'",
|
|
117
|
+
"connect-src 'self' https://your-api.com",
|
|
118
|
+
].join('; ')
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
async headers() {
|
|
124
|
+
return [{ source: '/(.*)', headers: securityHeaders }];
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Input Validation with Zod
|
|
130
|
+
|
|
131
|
+
**Validate Server Action and API route inputs with Zod** — client-side validation is UX, not security:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { z } from 'zod';
|
|
135
|
+
|
|
136
|
+
const CreatePostSchema = z.object({
|
|
137
|
+
title: z.string().min(1).max(200),
|
|
138
|
+
content: z.string().min(1).max(10000),
|
|
139
|
+
slug: z.string().regex(/^[a-z0-9-]+$/), // Allowlist pattern
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
'use server'
|
|
143
|
+
export async function createPost(formData: FormData) {
|
|
144
|
+
const session = await getServerSession(authOptions);
|
|
145
|
+
if (!session) throw new Error('Unauthorized');
|
|
146
|
+
|
|
147
|
+
const parsed = CreatePostSchema.safeParse({
|
|
148
|
+
title: formData.get('title'),
|
|
149
|
+
content: formData.get('content'),
|
|
150
|
+
slug: formData.get('slug'),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!parsed.success) {
|
|
154
|
+
return { error: parsed.error.flatten() };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await db.posts.create({ data: { ...parsed.data, authorId: session.user.id } });
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## ASVS Controls for Next.js Projects
|
|
164
|
+
|
|
165
|
+
| ASVS Ref | Control | Next.js Implementation |
|
|
166
|
+
|----------|---------|----------------------|
|
|
167
|
+
| V4.1.1 | Authentication on all endpoints | `getServerSession()` in every route handler and Server Action |
|
|
168
|
+
| V4.2.1 | Object-level authorisation | Check `resource.userId === session.user.id` before returning/modifying |
|
|
169
|
+
| V5.1.3 | Input validation | Zod schemas on all Server Action inputs |
|
|
170
|
+
| V7.1.1 | Log security events | Log auth events in NextAuth callbacks |
|
|
171
|
+
| V9.1.1 | TLS everywhere | Enforced by Vercel/host; add HSTS header |
|
|
172
|
+
| V14.4.1 | Security headers | `securityHeaders` in next.config.js |
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Common Next.js Vulnerabilities (2026)
|
|
177
|
+
|
|
178
|
+
1. **Missing auth on Server Actions** — the most common finding in Next.js apps
|
|
179
|
+
2. **IDOR via ID in URL params** — `/api/users/[id]` without ownership check
|
|
180
|
+
3. **Secrets leaked to Client Components** — passed as props or in `getServerSideProps` return
|
|
181
|
+
4. **No rate limiting on Server Actions** — can be abused for enumeration or spam
|
|
182
|
+
5. **Unsafe redirect in Next.js redirects** — `redirect(userSuppliedUrl)` allows open redirect
|
|
183
|
+
6. **Environment variables in client bundle** — `NEXT_PUBLIC_` prefix used for secrets
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Recommended Security Stack (2026)
|
|
188
|
+
|
|
189
|
+
| Category | Recommended |
|
|
190
|
+
|----------|-------------|
|
|
191
|
+
| Authentication | NextAuth.js v5 / Auth.js, Clerk, Lucia |
|
|
192
|
+
| Input validation | Zod, Valibot |
|
|
193
|
+
| Rate limiting | Upstash Ratelimit, @arcjet/next |
|
|
194
|
+
| Security headers | next-safe (generates CSP automatically) |
|
|
195
|
+
| CSRF | Built-in for Server Actions; csrf-csrf for API routes |
|
|
196
|
+
| Secrets | Vercel Environment Variables, Doppler, Infisical |
|
|
197
|
+
| ORM (injection-safe) | Prisma, Drizzle ORM |
|
|
198
|
+
| Image upload validation | sharp + file-type |
|
package/stacks/nodejs.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Node.js (generic) Security Profile
|
|
2
|
+
|
|
3
|
+
**Runtime:** Node.js 18+ (LTS recommended)
|
|
4
|
+
**ASVS Baseline:** L2
|
|
5
|
+
|
|
6
|
+
Use this profile when `package.json` exists but no specific framework (Next.js, Express, etc.)
|
|
7
|
+
was detected. Prefer a stack-specific profile from `stacks/` when you know your framework.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Core practices
|
|
12
|
+
|
|
13
|
+
- Validate all inputs server-side; use **Zod**, **Joi**, or **express-validator** as appropriate.
|
|
14
|
+
- Use **parameterised queries** or an ORM (**Prisma**, **Drizzle**, **TypeORM**); never concatenate user input into SQL.
|
|
15
|
+
- Store secrets in environment variables loaded at runtime, or a secrets manager — never commit `.env` with real values.
|
|
16
|
+
- Use **bcrypt** (cost ≥ 12) or **Argon2id** for password hashing.
|
|
17
|
+
- Prefer **helmet** (Express) or framework-specific security middleware for headers.
|
|
18
|
+
- Run **npm audit** / **Snyk** / **Dependabot** on every PR.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## When to switch to a stack profile
|
|
23
|
+
|
|
24
|
+
| If you use | Read |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Next.js | `stacks/nextjs.md` |
|
|
27
|
+
| Express | `stacks/express.md` |
|
|
28
|
+
| NestJS | Nest docs + OWASP ASVS; align with Express patterns for middleware |
|