@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
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaademos/secure-sdlc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Secure SDLC agent team — CLI to scaffold docs, hooks, CI, and MCP-ready security workflows",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"secure-sdlc": "./cli/bin/secure-sdlc.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli/bin",
|
|
11
|
+
"cli/src",
|
|
12
|
+
"mcp/package.json",
|
|
13
|
+
"mcp/README.md",
|
|
14
|
+
"mcp/src",
|
|
15
|
+
"docs/templates",
|
|
16
|
+
"hooks",
|
|
17
|
+
"stacks",
|
|
18
|
+
"warp-workflows",
|
|
19
|
+
".github/workflows/secure-sdlc-gate.yml",
|
|
20
|
+
".cursor/rules",
|
|
21
|
+
".claude/agents",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"CLAUDE.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepack": "node cli/bin/secure-sdlc.js --version",
|
|
29
|
+
"sdlc": "node cli/bin/secure-sdlc.js",
|
|
30
|
+
"test:pack": "npm pack --dry-run --ignore-scripts 2>&1"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"security",
|
|
34
|
+
"sdlc",
|
|
35
|
+
"appsec",
|
|
36
|
+
"compliance",
|
|
37
|
+
"owasp",
|
|
38
|
+
"asvs",
|
|
39
|
+
"claude",
|
|
40
|
+
"cursor",
|
|
41
|
+
"mcp",
|
|
42
|
+
"ai-coding",
|
|
43
|
+
"secure-by-default"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Kaademos/secure-sdlc-agents.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/Kaademos/secure-sdlc-agents/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/Kaademos/secure-sdlc-agents#readme",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
62
|
+
"chalk": "^5.3.0",
|
|
63
|
+
"commander": "^12.0.0",
|
|
64
|
+
"inquirer": "^9.2.0",
|
|
65
|
+
"ora": "^8.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/stacks/django.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Django Security Profile
|
|
2
|
+
|
|
3
|
+
**Framework:** Django (4.x / 5.x)
|
|
4
|
+
**Language:** Python 3.10+
|
|
5
|
+
**ASVS Baseline:** L2
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Django's Built-In Security — Don't Disable It
|
|
10
|
+
|
|
11
|
+
Django ships with more security defaults than most frameworks. The most common vulnerability
|
|
12
|
+
pattern in Django apps is **disabling or misconfiguring built-in protections**, not missing them.
|
|
13
|
+
|
|
14
|
+
### Never disable these:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# settings.py — NEVER set these to False in production
|
|
18
|
+
|
|
19
|
+
# CSRF protection
|
|
20
|
+
MIDDLEWARE = [
|
|
21
|
+
...
|
|
22
|
+
'django.middleware.csrf.CsrfViewMiddleware', # Never remove
|
|
23
|
+
...
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Clickjacking protection
|
|
27
|
+
X_FRAME_OPTIONS = 'DENY' # Default; never change to 'ALLOWALL'
|
|
28
|
+
|
|
29
|
+
# Secure cookies
|
|
30
|
+
SESSION_COOKIE_SECURE = True # HTTPS only
|
|
31
|
+
CSRF_COOKIE_SECURE = True
|
|
32
|
+
SESSION_COOKIE_HTTPONLY = True # Prevents JS access to session cookie
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Production Settings Checklist
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# settings.py — required for production
|
|
39
|
+
|
|
40
|
+
DEBUG = False # Non-negotiable
|
|
41
|
+
ALLOWED_HOSTS = ['yourdomain.com'] # Explicit, never ['*']
|
|
42
|
+
SECRET_KEY = env('SECRET_KEY') # From environment, never in code
|
|
43
|
+
|
|
44
|
+
# HTTPS enforcement
|
|
45
|
+
SECURE_SSL_REDIRECT = True
|
|
46
|
+
SECURE_HSTS_SECONDS = 63072000 # 2 years
|
|
47
|
+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
48
|
+
SECURE_HSTS_PRELOAD = True
|
|
49
|
+
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # If behind load balancer
|
|
50
|
+
|
|
51
|
+
# Session
|
|
52
|
+
SESSION_COOKIE_AGE = 3600 # 1 hour default; set appropriately
|
|
53
|
+
SESSION_EXPIRE_AT_BROWSER_CLOSE = True # Optional but good for sensitive apps
|
|
54
|
+
|
|
55
|
+
# Content security
|
|
56
|
+
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
57
|
+
SECURE_BROWSER_XSS_FILTER = True # Deprecated in modern browsers but harmless
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Authentication
|
|
63
|
+
|
|
64
|
+
Django's built-in auth is solid. Common mistakes:
|
|
65
|
+
|
|
66
|
+
### Password Storage (Already Handled by Django)
|
|
67
|
+
|
|
68
|
+
Django uses PBKDF2 by default. For higher assurance, switch to Argon2:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# settings.py
|
|
72
|
+
PASSWORD_HASHERS = [
|
|
73
|
+
'django.contrib.auth.hashers.Argon2PasswordHasher', # Best
|
|
74
|
+
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', # Good
|
|
75
|
+
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # Default — acceptable
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Minimum password validation
|
|
79
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
80
|
+
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
|
81
|
+
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
82
|
+
'OPTIONS': {'min_length': 12}},
|
|
83
|
+
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
|
84
|
+
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Access Control — Django Views and DRF
|
|
91
|
+
|
|
92
|
+
### Function-Based Views
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from django.contrib.auth.decorators import login_required
|
|
96
|
+
from django.core.exceptions import PermissionDenied
|
|
97
|
+
|
|
98
|
+
@login_required # Redirects unauthenticated users to LOGIN_URL
|
|
99
|
+
def my_view(request):
|
|
100
|
+
# IDOR check: verify the requested object belongs to request.user
|
|
101
|
+
post = get_object_or_404(Post, pk=pk)
|
|
102
|
+
if post.author != request.user:
|
|
103
|
+
raise PermissionDenied # Returns 403
|
|
104
|
+
...
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Django REST Framework
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from rest_framework.permissions import IsAuthenticated
|
|
111
|
+
from rest_framework.views import APIView
|
|
112
|
+
|
|
113
|
+
class PostDetailView(APIView):
|
|
114
|
+
permission_classes = [IsAuthenticated]
|
|
115
|
+
|
|
116
|
+
def get(self, request, pk):
|
|
117
|
+
# ✓ Object-level permission check
|
|
118
|
+
post = get_object_or_404(Post, pk=pk)
|
|
119
|
+
if post.author != request.user:
|
|
120
|
+
return Response(status=403)
|
|
121
|
+
serializer = PostSerializer(post)
|
|
122
|
+
return Response(serializer.data)
|
|
123
|
+
|
|
124
|
+
# Or using DRF object-level permissions:
|
|
125
|
+
class IsOwner(BasePermission):
|
|
126
|
+
def has_object_permission(self, request, view, obj):
|
|
127
|
+
return obj.author == request.user
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Never use `DEFAULT_AUTHENTICATION_CLASSES = []`** or **`DEFAULT_PERMISSION_CLASSES = []`** in
|
|
131
|
+
production DRF settings — these remove authentication and permission checks globally.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## ORM — Avoiding the Rare Django SQL Injection
|
|
136
|
+
|
|
137
|
+
Django's ORM is safe by default. Injection is only possible when using `.raw()` or `extra()`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# ✓ Safe — ORM parameterises automatically
|
|
141
|
+
Post.objects.filter(title=user_input)
|
|
142
|
+
|
|
143
|
+
# ✗ Unsafe — direct string formatting in raw query
|
|
144
|
+
Post.objects.raw(f"SELECT * FROM posts WHERE title = '{user_input}'")
|
|
145
|
+
|
|
146
|
+
# ✓ Safe raw query — use %s parameterisation
|
|
147
|
+
Post.objects.raw("SELECT * FROM posts WHERE title = %s", [user_input])
|
|
148
|
+
|
|
149
|
+
# ✗ Unsafe extra()
|
|
150
|
+
Post.objects.extra(where=[f"title = '{user_input}'"])
|
|
151
|
+
|
|
152
|
+
# ✓ Safe extra()
|
|
153
|
+
Post.objects.extra(where=["title = %s"], params=[user_input])
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## CSRF for APIs
|
|
159
|
+
|
|
160
|
+
If you're building a REST API consumed by non-browser clients, configure CSRF correctly:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# For DRF APIs using session auth from a browser:
|
|
164
|
+
# Keep CSRF enabled — use CsrfExemptSessionAuthentication or the Django CSRF view decorator
|
|
165
|
+
|
|
166
|
+
# For DRF APIs using token auth (not cookies):
|
|
167
|
+
# CSRF protection is not needed — tokens in Authorization header are CSRF-safe by design
|
|
168
|
+
|
|
169
|
+
# For hybrid (some browser, some API clients):
|
|
170
|
+
# Exempt specific views using @csrf_exempt and compensate with other controls (CORS, Origin check)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Secrets Management
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# ✗ Never
|
|
179
|
+
SECRET_KEY = 'hardcoded-secret-key'
|
|
180
|
+
DATABASE_URL = 'postgresql://user:password@localhost/db'
|
|
181
|
+
|
|
182
|
+
# ✓ Use django-environ or python-decouple
|
|
183
|
+
import environ
|
|
184
|
+
env = environ.Env()
|
|
185
|
+
environ.Env.read_env() # Reads .env for local dev (never commit .env)
|
|
186
|
+
|
|
187
|
+
SECRET_KEY = env('SECRET_KEY')
|
|
188
|
+
DATABASE_URL = env('DATABASE_URL')
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## ASVS Controls for Django Projects
|
|
194
|
+
|
|
195
|
+
| ASVS Ref | Control | Django Implementation |
|
|
196
|
+
|----------|---------|----------------------|
|
|
197
|
+
| V2.1.1 | Password complexity | `AUTH_PASSWORD_VALIDATORS` |
|
|
198
|
+
| V3.3.1 | Session invalidation on logout | `django.contrib.auth.logout()` clears session |
|
|
199
|
+
| V4.1.1 | Auth on endpoints | `@login_required` / `permission_classes` |
|
|
200
|
+
| V4.2.1 | Object-level auth | Explicit ownership checks before returning objects |
|
|
201
|
+
| V5.3.4 | No SQL injection | Use ORM; avoid `.raw()` with user input |
|
|
202
|
+
| V14.4.1 | Security headers | Django SecurityMiddleware |
|
|
203
|
+
| V14.4.5 | CSRF | CsrfViewMiddleware (enabled by default) |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Recommended Tools
|
|
208
|
+
|
|
209
|
+
| Category | Tool |
|
|
210
|
+
|----------|------|
|
|
211
|
+
| SAST | Bandit, Semgrep (Django rules) |
|
|
212
|
+
| DAST | OWASP ZAP |
|
|
213
|
+
| Dependency scan | pip-audit, Safety |
|
|
214
|
+
| Secrets | python-decouple, django-environ |
|
|
215
|
+
| 2FA | django-two-factor-auth, django-otp |
|
|
216
|
+
| Rate limiting | django-ratelimit, django-axes (login lockout) |
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Express.js Security Profile
|
|
2
|
+
|
|
3
|
+
**Framework:** Express.js 4.x / 5.x
|
|
4
|
+
**Language:** JavaScript / TypeScript (Node.js)
|
|
5
|
+
**ASVS Baseline:** L2
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Express has No Security Defaults — You Must Add Everything
|
|
10
|
+
|
|
11
|
+
Express is minimal by design. Unlike Django or Rails, it ships with no security headers, no CSRF
|
|
12
|
+
protection, no input validation, and no rate limiting. Every security control must be explicitly added.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Minimum Required Security Middleware
|
|
17
|
+
|
|
18
|
+
Add these to every new Express application:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import express from 'express';
|
|
22
|
+
import helmet from 'helmet';
|
|
23
|
+
import { rateLimit } from 'express-rate-limit';
|
|
24
|
+
import cors from 'cors';
|
|
25
|
+
import { doubleCsrf } from 'csrf-csrf';
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
|
|
29
|
+
// 1. Security headers (CSP, HSTS, X-Frame-Options, etc.)
|
|
30
|
+
app.use(helmet({
|
|
31
|
+
contentSecurityPolicy: {
|
|
32
|
+
directives: {
|
|
33
|
+
defaultSrc: ["'self'"],
|
|
34
|
+
scriptSrc: ["'self'"],
|
|
35
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
36
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
hsts: {
|
|
40
|
+
maxAge: 63072000,
|
|
41
|
+
includeSubDomains: true,
|
|
42
|
+
preload: true,
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// 2. Rate limiting — global baseline
|
|
47
|
+
const globalLimiter = rateLimit({
|
|
48
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
49
|
+
max: 100,
|
|
50
|
+
standardHeaders: true,
|
|
51
|
+
legacyHeaders: false,
|
|
52
|
+
message: { error: 'Too many requests' },
|
|
53
|
+
});
|
|
54
|
+
app.use(globalLimiter);
|
|
55
|
+
|
|
56
|
+
// 3. Strict rate limiting for auth endpoints
|
|
57
|
+
const authLimiter = rateLimit({
|
|
58
|
+
windowMs: 15 * 60 * 1000,
|
|
59
|
+
max: 10, // Only 10 login attempts per 15 minutes per IP
|
|
60
|
+
skipSuccessfulRequests: true,
|
|
61
|
+
});
|
|
62
|
+
app.use('/auth', authLimiter);
|
|
63
|
+
|
|
64
|
+
// 4. CORS — explicit origins only
|
|
65
|
+
app.use(cors({
|
|
66
|
+
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [],
|
|
67
|
+
credentials: true,
|
|
68
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
// 5. CSRF protection (for session-based auth)
|
|
72
|
+
const { doubleCsrfProtection } = doubleCsrf({
|
|
73
|
+
getSecret: () => process.env.CSRF_SECRET,
|
|
74
|
+
cookieName: '__Host-psifi.x-csrf-token',
|
|
75
|
+
cookieOptions: { secure: true, sameSite: 'strict' },
|
|
76
|
+
});
|
|
77
|
+
app.use(doubleCsrfProtection);
|
|
78
|
+
|
|
79
|
+
// 6. Body parsing with size limits
|
|
80
|
+
app.use(express.json({ limit: '10kb' })); // Prevent JSON body DoS
|
|
81
|
+
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Authentication
|
|
87
|
+
|
|
88
|
+
Express has no built-in auth. Options:
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
// Using passport.js with bcrypt
|
|
92
|
+
import passport from 'passport';
|
|
93
|
+
import { Strategy as LocalStrategy } from 'passport-local';
|
|
94
|
+
import bcrypt from 'bcrypt';
|
|
95
|
+
|
|
96
|
+
passport.use(new LocalStrategy(async (username, password, done) => {
|
|
97
|
+
try {
|
|
98
|
+
const user = await User.findOne({ username });
|
|
99
|
+
if (!user) {
|
|
100
|
+
// ✓ Same error for wrong username OR wrong password (user enumeration prevention)
|
|
101
|
+
return done(null, false, { message: 'Invalid credentials' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isValid = await bcrypt.compare(password, user.passwordHash);
|
|
105
|
+
if (!isValid) {
|
|
106
|
+
return done(null, false, { message: 'Invalid credentials' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return done(null, user);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return done(err);
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
// Password hashing — minimum cost factor 12
|
|
116
|
+
const BCRYPT_ROUNDS = 12;
|
|
117
|
+
const hash = await bcrypt.hash(password, BCRYPT_ROUNDS);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Input Validation with Zod
|
|
123
|
+
|
|
124
|
+
`req.body`, `req.params`, and `req.query` are **completely untyped** in Express. Validate everything:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { z } from 'zod';
|
|
128
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
129
|
+
|
|
130
|
+
const CreateUserSchema = z.object({
|
|
131
|
+
username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9_]+$/),
|
|
132
|
+
email: z.string().email().max(255),
|
|
133
|
+
password: z.string().min(12).max(128),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Reusable validation middleware factory
|
|
137
|
+
const validate = (schema: z.ZodSchema) => (req: Request, res: Response, next: NextFunction) => {
|
|
138
|
+
const result = schema.safeParse(req.body);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
return res.status(400).json({
|
|
141
|
+
error: 'Validation failed',
|
|
142
|
+
details: result.error.flatten().fieldErrors // Safe to return — no internal info
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
req.body = result.data; // Replace with validated/parsed data
|
|
146
|
+
next();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
router.post('/users', validate(CreateUserSchema), async (req, res) => {
|
|
150
|
+
// req.body is now validated and typed
|
|
151
|
+
const { username, email, password } = req.body;
|
|
152
|
+
...
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Error Handling — No Stack Trace Leakage
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
// ✗ Express default error handler sends the full error in development
|
|
162
|
+
// ✗ Custom handler that leaks details
|
|
163
|
+
app.use((err, req, res, next) => {
|
|
164
|
+
res.status(500).json({ error: err.message, stack: err.stack }); // Never in production
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ✓ Generic user response, detailed server-side logging
|
|
168
|
+
import { logger } from './logger';
|
|
169
|
+
|
|
170
|
+
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
171
|
+
logger.error({
|
|
172
|
+
message: err.message,
|
|
173
|
+
stack: err.stack,
|
|
174
|
+
path: req.path,
|
|
175
|
+
method: req.method,
|
|
176
|
+
userId: req.user?.id ?? 'anonymous',
|
|
177
|
+
requestId: req.id,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
res.status(500).json({ error: 'An internal error occurred' });
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Database Queries — Never String Concatenate
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
// Using pg (node-postgres)
|
|
190
|
+
|
|
191
|
+
// ✗ SQL injection
|
|
192
|
+
const user = await pool.query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
193
|
+
|
|
194
|
+
// ✓ Parameterised query
|
|
195
|
+
const user = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
196
|
+
|
|
197
|
+
// Using an ORM (Prisma, Drizzle) — safe by default
|
|
198
|
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## ASVS Controls for Express Projects
|
|
204
|
+
|
|
205
|
+
| ASVS Ref | Control | Express Implementation |
|
|
206
|
+
|----------|---------|----------------------|
|
|
207
|
+
| V4.1.1 | Auth middleware | passport.js + per-route authentication middleware |
|
|
208
|
+
| V4.2.1 | Object-level auth | Ownership check in every route handler |
|
|
209
|
+
| V5.1.3 | Input validation | Zod validation middleware |
|
|
210
|
+
| V13.2.5 | Rate limiting | express-rate-limit per endpoint |
|
|
211
|
+
| V14.4.1 | Security headers | helmet() |
|
|
212
|
+
| V14.4.5 | CSRF | csrf-csrf or csurf |
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Recommended Security Stack (2026)
|
|
217
|
+
|
|
218
|
+
| Category | Recommended |
|
|
219
|
+
|----------|-------------|
|
|
220
|
+
| Security headers | helmet |
|
|
221
|
+
| Rate limiting | express-rate-limit |
|
|
222
|
+
| CSRF | csrf-csrf |
|
|
223
|
+
| Auth | passport.js, express-jwt |
|
|
224
|
+
| Input validation | zod, express-validator |
|
|
225
|
+
| Password hashing | bcrypt (min rounds: 12) |
|
|
226
|
+
| Session | express-session + connect-redis |
|
|
227
|
+
| ORM | Prisma, Drizzle ORM |
|
|
228
|
+
| Logging | pino (structured JSON) |
|
|
229
|
+
| Secrets | dotenv-vault, Doppler |
|