clearctx 3.0.0 → 3.1.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/README.md +1 -0
- package/bin/setup.js +33 -1
- package/package.json +3 -2
- package/skills/api-design/SKILL.md +796 -0
- package/skills/devops/SKILL.md +1043 -0
- package/skills/index.json +53 -0
- package/skills/nodejs-backend/SKILL.md +853 -0
- package/skills/postgresql/SKILL.md +315 -0
- package/skills/react-frontend/SKILL.md +683 -0
- package/skills/security/SKILL.md +1000 -0
- package/skills/testing-qa/SKILL.md +842 -0
- package/skills/typescript/SKILL.md +932 -0
- package/src/mcp-server.js +126 -1
- package/src/prompts.js +47 -2
- package/src/skill-registry.js +182 -0
- package/src/stream-session.js +22 -2
- package/STRATEGY.md +0 -485
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security
|
|
3
|
+
description: Production-grade application security covering authentication, authorization, input validation, XSS/CSRF/SQLi prevention, and secrets management
|
|
4
|
+
domain: security
|
|
5
|
+
keywords: [security, authentication, authorization, jwt, xss, csrf, sql-injection, encryption, cors, helmet]
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Security Expertise Skill
|
|
10
|
+
|
|
11
|
+
## Worker Context
|
|
12
|
+
|
|
13
|
+
You are a security specialist worker. Your role is to implement production-grade security controls following industry best practices. Every security decision has real consequences — a single vulnerability can compromise the entire application.
|
|
14
|
+
|
|
15
|
+
### Authentication
|
|
16
|
+
|
|
17
|
+
**JWT Token Strategy**
|
|
18
|
+
|
|
19
|
+
CRITICAL: Implement dual-token architecture:
|
|
20
|
+
- **Access token**: 15min expiry, stored in memory or httpOnly cookie
|
|
21
|
+
- **Refresh token**: 7d expiry, stored in httpOnly, secure, SameSite=Strict cookie
|
|
22
|
+
|
|
23
|
+
NEVER store JWT in localStorage (XSS-accessible).
|
|
24
|
+
|
|
25
|
+
**Token Structure**
|
|
26
|
+
```javascript
|
|
27
|
+
{
|
|
28
|
+
sub: "user-id",
|
|
29
|
+
iat: 1234567890,
|
|
30
|
+
exp: 1234568790,
|
|
31
|
+
role: "admin"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Password Hashing**
|
|
36
|
+
- Use bcrypt with cost factor 12+ (or argon2id)
|
|
37
|
+
- NEVER use MD5, SHA1, or SHA256 for passwords
|
|
38
|
+
- Check passwords against breach databases (HaveIBeenPwned API)
|
|
39
|
+
- Minimum length: 8 characters
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// GOOD
|
|
43
|
+
const bcrypt = require('bcrypt');
|
|
44
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
45
|
+
|
|
46
|
+
// BAD
|
|
47
|
+
const hashedPassword = crypto.createHash('md5').update(password).digest('hex');
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Authorization
|
|
51
|
+
|
|
52
|
+
**RBAC (Role-Based Access Control)**
|
|
53
|
+
|
|
54
|
+
Implement three-tier model:
|
|
55
|
+
1. **Roles**: admin, user, viewer
|
|
56
|
+
2. **Permissions**: read, write, delete
|
|
57
|
+
3. **Resources**: orders, users, reports
|
|
58
|
+
|
|
59
|
+
**Middleware Pattern**
|
|
60
|
+
```javascript
|
|
61
|
+
// GOOD: Server-side role check
|
|
62
|
+
function requireRole(role) {
|
|
63
|
+
return (req, res, next) => {
|
|
64
|
+
if (req.user.role !== role) {
|
|
65
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
66
|
+
}
|
|
67
|
+
next();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Resource-level authorization
|
|
72
|
+
function requireOwnership(req, res, next) {
|
|
73
|
+
if (req.user.id !== resource.ownerId) {
|
|
74
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
75
|
+
}
|
|
76
|
+
next();
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
NEVER rely solely on client-side role checks — always enforce server-side.
|
|
81
|
+
|
|
82
|
+
### Input Validation
|
|
83
|
+
|
|
84
|
+
CRITICAL: Validate ALL inputs server-side:
|
|
85
|
+
- req.body
|
|
86
|
+
- req.params
|
|
87
|
+
- req.query
|
|
88
|
+
- headers
|
|
89
|
+
|
|
90
|
+
**Validation Strategy**
|
|
91
|
+
- Whitelist allowed values, NEVER blacklist
|
|
92
|
+
- Use schema validation (Zod, Joi) at route level
|
|
93
|
+
- Sanitize HTML with DOMPurify before rendering
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
// GOOD: Schema validation
|
|
97
|
+
const userSchema = z.object({
|
|
98
|
+
email: z.string().email(),
|
|
99
|
+
age: z.number().min(18).max(120),
|
|
100
|
+
role: z.enum(['admin', 'user', 'viewer'])
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
app.post('/users', (req, res) => {
|
|
104
|
+
const result = userSchema.safeParse(req.body);
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
return res.status(400).json({ error: result.error });
|
|
107
|
+
}
|
|
108
|
+
// Proceed with validated data
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// BAD: No validation
|
|
112
|
+
app.post('/users', (req, res) => {
|
|
113
|
+
const { email, age, role } = req.body;
|
|
114
|
+
db.createUser({ email, age, role }); // Vulnerable
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### SQL Injection Prevention
|
|
119
|
+
|
|
120
|
+
CRITICAL: ALWAYS use parameterized queries. NEVER concatenate user input into SQL strings.
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// GOOD: Parameterized query
|
|
124
|
+
const result = await db.query(
|
|
125
|
+
'SELECT * FROM users WHERE id = $1 AND status = $2',
|
|
126
|
+
[userId, status]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// BAD: String concatenation - VULNERABLE TO SQL INJECTION
|
|
130
|
+
const result = await db.query(
|
|
131
|
+
'SELECT * FROM users WHERE id = ' + userId + ' AND status = "' + status + '"'
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Query Builder Safety**
|
|
136
|
+
```javascript
|
|
137
|
+
// GOOD: Using query builder properly
|
|
138
|
+
const user = await db('users')
|
|
139
|
+
.where({ id: userId })
|
|
140
|
+
.first();
|
|
141
|
+
|
|
142
|
+
// BAD: Raw queries with interpolation
|
|
143
|
+
const user = await db.raw(`SELECT * FROM users WHERE id = ${userId}`);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### XSS Prevention
|
|
147
|
+
|
|
148
|
+
**Output Encoding**
|
|
149
|
+
|
|
150
|
+
CRITICAL: Encode all user content before rendering.
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
// GOOD: Using DOMPurify for rich text
|
|
154
|
+
import DOMPurify from 'dompurify';
|
|
155
|
+
const clean = DOMPurify.sanitize(userContent);
|
|
156
|
+
|
|
157
|
+
// BAD: Direct innerHTML - VULNERABLE TO XSS
|
|
158
|
+
element.innerHTML = userContent;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Content Security Policy**
|
|
162
|
+
```javascript
|
|
163
|
+
app.use(helmet.contentSecurityPolicy({
|
|
164
|
+
directives: {
|
|
165
|
+
defaultSrc: ["'self'"],
|
|
166
|
+
scriptSrc: ["'self'"],
|
|
167
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
168
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
169
|
+
connectSrc: ["'self'"],
|
|
170
|
+
fontSrc: ["'self'"],
|
|
171
|
+
objectSrc: ["'none'"],
|
|
172
|
+
mediaSrc: ["'self'"],
|
|
173
|
+
frameSrc: ["'none'"]
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
NEVER use:
|
|
179
|
+
- `dangerouslySetInnerHTML` with unsanitized input
|
|
180
|
+
- `eval()` or `new Function()` with user input
|
|
181
|
+
- `onclick="user_data"` inline event handlers
|
|
182
|
+
|
|
183
|
+
### CSRF Protection
|
|
184
|
+
|
|
185
|
+
**Defense Layers**
|
|
186
|
+
|
|
187
|
+
1. **SameSite Cookies** (Primary defense)
|
|
188
|
+
```javascript
|
|
189
|
+
res.cookie('session', token, {
|
|
190
|
+
httpOnly: true,
|
|
191
|
+
secure: true,
|
|
192
|
+
sameSite: 'strict',
|
|
193
|
+
maxAge: 604800000 // 7 days
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
2. **CSRF Tokens** (Double-submit cookie pattern)
|
|
198
|
+
```javascript
|
|
199
|
+
const csrf = require('csurf');
|
|
200
|
+
app.use(csrf({ cookie: true }));
|
|
201
|
+
|
|
202
|
+
app.get('/form', (req, res) => {
|
|
203
|
+
res.render('form', { csrfToken: req.csrfToken() });
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
3. **Origin/Referer Verification**
|
|
208
|
+
```javascript
|
|
209
|
+
function verifyOrigin(req, res, next) {
|
|
210
|
+
const origin = req.get('Origin') || req.get('Referer');
|
|
211
|
+
if (!origin || !origin.startsWith('https://yourdomain.com')) {
|
|
212
|
+
return res.status(403).json({ error: 'Invalid origin' });
|
|
213
|
+
}
|
|
214
|
+
next();
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Password Security
|
|
219
|
+
|
|
220
|
+
**Hashing Parameters**
|
|
221
|
+
- bcrypt cost: 12 minimum (increases computation time exponentially)
|
|
222
|
+
- argon2id: memory=64MB, iterations=3, parallelism=4
|
|
223
|
+
|
|
224
|
+
**Password Requirements**
|
|
225
|
+
- Minimum 8 characters
|
|
226
|
+
- Check against breached password lists (HaveIBeenPwned API)
|
|
227
|
+
- NEVER store plaintext
|
|
228
|
+
- NEVER send passwords in URL/query params
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// GOOD: Secure password handling
|
|
232
|
+
const bcrypt = require('bcrypt');
|
|
233
|
+
const SALT_ROUNDS = 12;
|
|
234
|
+
|
|
235
|
+
async function hashPassword(password) {
|
|
236
|
+
return await bcrypt.hash(password, SALT_ROUNDS);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function verifyPassword(password, hash) {
|
|
240
|
+
return await bcrypt.compare(password, hash);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// BAD: Insecure hashing
|
|
244
|
+
const hash = crypto.createHash('sha256').update(password).digest('hex');
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### HTTPS & Transport Security
|
|
248
|
+
|
|
249
|
+
**Force HTTPS**
|
|
250
|
+
```javascript
|
|
251
|
+
app.use((req, res, next) => {
|
|
252
|
+
if (req.header('x-forwarded-proto') !== 'https') {
|
|
253
|
+
return res.redirect(`https://${req.header('host')}${req.url}`);
|
|
254
|
+
}
|
|
255
|
+
next();
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**HSTS Header**
|
|
260
|
+
```javascript
|
|
261
|
+
app.use(helmet.hsts({
|
|
262
|
+
maxAge: 31536000, // 1 year
|
|
263
|
+
includeSubDomains: true,
|
|
264
|
+
preload: true
|
|
265
|
+
}));
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Secure Cookie Flags**
|
|
269
|
+
```javascript
|
|
270
|
+
// GOOD
|
|
271
|
+
res.cookie('token', value, {
|
|
272
|
+
secure: true, // HTTPS only
|
|
273
|
+
httpOnly: true, // Not accessible via JavaScript
|
|
274
|
+
sameSite: 'strict' // CSRF protection
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// BAD
|
|
278
|
+
res.cookie('token', value); // Vulnerable
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Rate Limiting
|
|
282
|
+
|
|
283
|
+
**Tiered Rate Limits**
|
|
284
|
+
|
|
285
|
+
| Endpoint Type | Limit | Window |
|
|
286
|
+
|---------------|-------|--------|
|
|
287
|
+
| Public endpoints | 100 req | 15 min |
|
|
288
|
+
| Authenticated | 1000 req | 15 min |
|
|
289
|
+
| Auth routes (login/register) | 5 req | 15 min |
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
const rateLimit = require('express-rate-limit');
|
|
293
|
+
|
|
294
|
+
// Auth endpoints
|
|
295
|
+
const authLimiter = rateLimit({
|
|
296
|
+
windowMs: 15 * 60 * 1000,
|
|
297
|
+
max: 5,
|
|
298
|
+
message: 'Too many login attempts, try again later',
|
|
299
|
+
standardHeaders: true,
|
|
300
|
+
legacyHeaders: false
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
app.post('/login', authLimiter, loginHandler);
|
|
304
|
+
|
|
305
|
+
// API endpoints
|
|
306
|
+
const apiLimiter = rateLimit({
|
|
307
|
+
windowMs: 15 * 60 * 1000,
|
|
308
|
+
max: 100,
|
|
309
|
+
keyGenerator: (req) => req.user?.id || req.ip
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Exponential Backoff**
|
|
314
|
+
```javascript
|
|
315
|
+
// Failed login tracking
|
|
316
|
+
const loginAttempts = new Map();
|
|
317
|
+
|
|
318
|
+
function getBackoffDelay(userId) {
|
|
319
|
+
const attempts = loginAttempts.get(userId) || 0;
|
|
320
|
+
return Math.min(Math.pow(2, attempts) * 1000, 30000); // Max 30s
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Return 429 status with Retry-After header on rate limit.
|
|
325
|
+
|
|
326
|
+
### Secrets Management
|
|
327
|
+
|
|
328
|
+
CRITICAL: NEVER commit secrets to version control.
|
|
329
|
+
|
|
330
|
+
**Environment Variables**
|
|
331
|
+
```bash
|
|
332
|
+
# .env (MUST be in .gitignore)
|
|
333
|
+
DATABASE_URL=postgresql://user:pass@localhost/db
|
|
334
|
+
JWT_SECRET=your-secret-key-here
|
|
335
|
+
API_KEY=your-api-key-here
|
|
336
|
+
|
|
337
|
+
# .env.example (commit this)
|
|
338
|
+
DATABASE_URL=
|
|
339
|
+
JWT_SECRET=
|
|
340
|
+
API_KEY=
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Loading Secrets**
|
|
344
|
+
```javascript
|
|
345
|
+
// GOOD
|
|
346
|
+
require('dotenv').config();
|
|
347
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
348
|
+
|
|
349
|
+
// BAD: Hardcoded secrets
|
|
350
|
+
const dbUrl = 'postgresql://user:password@localhost/db';
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Logging Secrets**
|
|
354
|
+
```javascript
|
|
355
|
+
// GOOD: Redact sensitive fields
|
|
356
|
+
const pino = require('pino');
|
|
357
|
+
const logger = pino({
|
|
358
|
+
redact: ['req.headers.authorization', 'password', 'token', 'apiKey']
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// BAD: Logging raw requests
|
|
362
|
+
console.log('Request:', req.body); // May contain passwords
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
NEVER:
|
|
366
|
+
- Commit .env files
|
|
367
|
+
- Put secrets in Dockerfile
|
|
368
|
+
- Log secrets (use Pino redact)
|
|
369
|
+
- Send secrets in error messages
|
|
370
|
+
|
|
371
|
+
**Secret Rotation**
|
|
372
|
+
- Rotate on suspected breach
|
|
373
|
+
- Rotate JWT secrets every 90 days
|
|
374
|
+
- Use different secrets per environment
|
|
375
|
+
|
|
376
|
+
### Security Headers
|
|
377
|
+
|
|
378
|
+
**Helmet.js Configuration**
|
|
379
|
+
```javascript
|
|
380
|
+
const helmet = require('helmet');
|
|
381
|
+
|
|
382
|
+
app.use(helmet({
|
|
383
|
+
contentSecurityPolicy: {
|
|
384
|
+
directives: {
|
|
385
|
+
defaultSrc: ["'self'"],
|
|
386
|
+
scriptSrc: ["'self'"]
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
hsts: {
|
|
390
|
+
maxAge: 31536000,
|
|
391
|
+
includeSubDomains: true
|
|
392
|
+
}
|
|
393
|
+
}));
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Critical Headers**
|
|
397
|
+
```javascript
|
|
398
|
+
X-Content-Type-Options: nosniff
|
|
399
|
+
X-Frame-Options: DENY
|
|
400
|
+
X-XSS-Protection: 1; mode=block
|
|
401
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
402
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### CORS Configuration
|
|
406
|
+
|
|
407
|
+
CRITICAL: NEVER use `origin: '*'` with credentials.
|
|
408
|
+
|
|
409
|
+
```javascript
|
|
410
|
+
// GOOD: Whitelist specific origins
|
|
411
|
+
const cors = require('cors');
|
|
412
|
+
|
|
413
|
+
app.use(cors({
|
|
414
|
+
origin: ['https://app.example.com', 'https://admin.example.com'],
|
|
415
|
+
credentials: true,
|
|
416
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
417
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
418
|
+
maxAge: 86400 // Preflight cache: 24 hours
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
// BAD: Permissive CORS with credentials
|
|
422
|
+
app.use(cors({
|
|
423
|
+
origin: '*',
|
|
424
|
+
credentials: true // VULNERABLE
|
|
425
|
+
}));
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Dynamic Origin Validation**
|
|
429
|
+
```javascript
|
|
430
|
+
const allowedOrigins = [
|
|
431
|
+
'https://app.example.com',
|
|
432
|
+
'https://admin.example.com'
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
app.use(cors({
|
|
436
|
+
origin: (origin, callback) => {
|
|
437
|
+
if (!origin || allowedOrigins.includes(origin)) {
|
|
438
|
+
callback(null, true);
|
|
439
|
+
} else {
|
|
440
|
+
callback(new Error('Not allowed by CORS'));
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
credentials: true
|
|
444
|
+
}));
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Error Handling
|
|
448
|
+
|
|
449
|
+
**Production vs Development**
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
// GOOD: Safe error responses
|
|
453
|
+
app.use((err, req, res, next) => {
|
|
454
|
+
logger.error(err); // Log full error server-side
|
|
455
|
+
|
|
456
|
+
if (process.env.NODE_ENV === 'production') {
|
|
457
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
458
|
+
} else {
|
|
459
|
+
res.status(500).json({
|
|
460
|
+
error: err.message,
|
|
461
|
+
stack: err.stack
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// BAD: Exposing sensitive information
|
|
467
|
+
app.use((err, req, res, next) => {
|
|
468
|
+
res.status(500).json({
|
|
469
|
+
error: err.message,
|
|
470
|
+
stack: err.stack,
|
|
471
|
+
query: req.query // May contain SQL or tokens
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
NEVER expose in production:
|
|
477
|
+
- Stack traces
|
|
478
|
+
- Database error messages
|
|
479
|
+
- File paths
|
|
480
|
+
- SQL queries
|
|
481
|
+
- Environment variables
|
|
482
|
+
|
|
483
|
+
## Conventions
|
|
484
|
+
|
|
485
|
+
### Response Format
|
|
486
|
+
```javascript
|
|
487
|
+
// Success
|
|
488
|
+
{ data: <result> }
|
|
489
|
+
|
|
490
|
+
// Error
|
|
491
|
+
{ error: <message> }
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### HTTP Status Codes
|
|
495
|
+
- 200: Success (read, update, delete)
|
|
496
|
+
- 201: Created
|
|
497
|
+
- 400: Bad request (validation failed)
|
|
498
|
+
- 401: Unauthorized (no/invalid token)
|
|
499
|
+
- 403: Forbidden (valid token, insufficient permissions)
|
|
500
|
+
- 404: Not found
|
|
501
|
+
- 409: Conflict (duplicate resource)
|
|
502
|
+
- 429: Too many requests (rate limited)
|
|
503
|
+
- 500: Internal server error
|
|
504
|
+
|
|
505
|
+
### Naming Conventions
|
|
506
|
+
- Database columns: snake_case
|
|
507
|
+
- JavaScript variables: camelCase
|
|
508
|
+
- Environment variables: UPPER_SNAKE_CASE
|
|
509
|
+
- File names: kebab-case
|
|
510
|
+
|
|
511
|
+
### Token Expiry Times
|
|
512
|
+
- Access token: 900 (15 minutes)
|
|
513
|
+
- Refresh token: 604800 (7 days)
|
|
514
|
+
- Remember-me token: 2592000 (30 days)
|
|
515
|
+
|
|
516
|
+
## Common Patterns
|
|
517
|
+
|
|
518
|
+
### 1. JWT Authentication Middleware
|
|
519
|
+
|
|
520
|
+
```javascript
|
|
521
|
+
const jwt = require('jsonwebtoken');
|
|
522
|
+
|
|
523
|
+
function authenticateToken(req, res, next) {
|
|
524
|
+
// Extract token from Authorization header or cookie
|
|
525
|
+
const authHeader = req.headers['authorization'];
|
|
526
|
+
const token = authHeader?.split(' ')[1] || req.cookies.accessToken;
|
|
527
|
+
|
|
528
|
+
if (!token) {
|
|
529
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const user = jwt.verify(token, process.env.JWT_SECRET);
|
|
534
|
+
req.user = user;
|
|
535
|
+
next();
|
|
536
|
+
} catch (err) {
|
|
537
|
+
if (err.name === 'TokenExpiredError') {
|
|
538
|
+
return res.status(401).json({ error: 'Token expired' });
|
|
539
|
+
}
|
|
540
|
+
return res.status(403).json({ error: 'Invalid token' });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Usage
|
|
545
|
+
app.get('/protected', authenticateToken, (req, res) => {
|
|
546
|
+
res.json({ data: req.user });
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### 2. Refresh Token Rotation
|
|
551
|
+
|
|
552
|
+
```javascript
|
|
553
|
+
async function refreshAccessToken(req, res) {
|
|
554
|
+
const refreshToken = req.cookies.refreshToken;
|
|
555
|
+
|
|
556
|
+
if (!refreshToken) {
|
|
557
|
+
return res.status(401).json({ error: 'Refresh token required' });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const user = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
|
562
|
+
|
|
563
|
+
// Issue new access token
|
|
564
|
+
const newAccessToken = jwt.sign(
|
|
565
|
+
{ sub: user.sub, role: user.role },
|
|
566
|
+
process.env.JWT_SECRET,
|
|
567
|
+
{ expiresIn: '15m' }
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// Rotate refresh token
|
|
571
|
+
const newRefreshToken = jwt.sign(
|
|
572
|
+
{ sub: user.sub },
|
|
573
|
+
process.env.JWT_REFRESH_SECRET,
|
|
574
|
+
{ expiresIn: '7d' }
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// Set new tokens
|
|
578
|
+
res.cookie('accessToken', newAccessToken, {
|
|
579
|
+
httpOnly: true,
|
|
580
|
+
secure: true,
|
|
581
|
+
sameSite: 'strict',
|
|
582
|
+
maxAge: 900000 // 15 min
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
res.cookie('refreshToken', newRefreshToken, {
|
|
586
|
+
httpOnly: true,
|
|
587
|
+
secure: true,
|
|
588
|
+
sameSite: 'strict',
|
|
589
|
+
maxAge: 604800000 // 7 days
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
res.json({ data: { accessToken: newAccessToken } });
|
|
593
|
+
} catch (err) {
|
|
594
|
+
res.status(403).json({ error: 'Invalid refresh token' });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### 3. Input Validation with Zod
|
|
600
|
+
|
|
601
|
+
```javascript
|
|
602
|
+
const { z } = require('zod');
|
|
603
|
+
|
|
604
|
+
// Define schemas
|
|
605
|
+
const createUserSchema = z.object({
|
|
606
|
+
email: z.string().email().max(255),
|
|
607
|
+
password: z.string().min(8).max(128),
|
|
608
|
+
name: z.string().min(1).max(100),
|
|
609
|
+
role: z.enum(['admin', 'user', 'viewer']).optional()
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const updateUserSchema = z.object({
|
|
613
|
+
name: z.string().min(1).max(100).optional(),
|
|
614
|
+
email: z.string().email().max(255).optional()
|
|
615
|
+
}).refine(data => Object.keys(data).length > 0, {
|
|
616
|
+
message: 'At least one field required'
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Validation middleware
|
|
620
|
+
function validate(schema) {
|
|
621
|
+
return (req, res, next) => {
|
|
622
|
+
const result = schema.safeParse(req.body);
|
|
623
|
+
if (!result.success) {
|
|
624
|
+
return res.status(400).json({
|
|
625
|
+
error: 'Validation failed',
|
|
626
|
+
details: result.error.errors
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
req.validatedData = result.data;
|
|
630
|
+
next();
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Usage
|
|
635
|
+
app.post('/users', validate(createUserSchema), async (req, res) => {
|
|
636
|
+
const user = await createUser(req.validatedData);
|
|
637
|
+
res.status(201).json({ data: user });
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 4. RBAC Permission Checker
|
|
642
|
+
|
|
643
|
+
```javascript
|
|
644
|
+
// Permission definitions
|
|
645
|
+
const PERMISSIONS = {
|
|
646
|
+
admin: ['users:read', 'users:write', 'users:delete', 'orders:read', 'orders:write'],
|
|
647
|
+
user: ['orders:read', 'orders:write'],
|
|
648
|
+
viewer: ['orders:read']
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
function requirePermission(permission) {
|
|
652
|
+
return (req, res, next) => {
|
|
653
|
+
const userPermissions = PERMISSIONS[req.user.role] || [];
|
|
654
|
+
|
|
655
|
+
if (!userPermissions.includes(permission)) {
|
|
656
|
+
return res.status(403).json({
|
|
657
|
+
error: 'Insufficient permissions',
|
|
658
|
+
required: permission,
|
|
659
|
+
has: userPermissions
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
next();
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Usage
|
|
668
|
+
app.delete('/users/:id',
|
|
669
|
+
authenticateToken,
|
|
670
|
+
requirePermission('users:delete'),
|
|
671
|
+
deleteUserHandler
|
|
672
|
+
);
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### 5. Secure Password Reset Flow
|
|
676
|
+
|
|
677
|
+
```javascript
|
|
678
|
+
const crypto = require('crypto');
|
|
679
|
+
|
|
680
|
+
// Generate reset token
|
|
681
|
+
async function initiatePasswordReset(email) {
|
|
682
|
+
const user = await findUserByEmail(email);
|
|
683
|
+
if (!user) {
|
|
684
|
+
// Don't reveal if email exists
|
|
685
|
+
return { success: true };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Generate secure token
|
|
689
|
+
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
690
|
+
const resetTokenHash = crypto
|
|
691
|
+
.createHash('sha256')
|
|
692
|
+
.update(resetToken)
|
|
693
|
+
.digest('hex');
|
|
694
|
+
|
|
695
|
+
// Store hashed token with expiry
|
|
696
|
+
await db('users').where({ id: user.id }).update({
|
|
697
|
+
reset_token_hash: resetTokenHash,
|
|
698
|
+
reset_token_expires: new Date(Date.now() + 3600000) // 1 hour
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Send email with plain token (only time it's visible)
|
|
702
|
+
await sendResetEmail(user.email, resetToken);
|
|
703
|
+
|
|
704
|
+
return { success: true };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Verify and reset password
|
|
708
|
+
async function resetPassword(token, newPassword) {
|
|
709
|
+
const tokenHash = crypto
|
|
710
|
+
.createHash('sha256')
|
|
711
|
+
.update(token)
|
|
712
|
+
.digest('hex');
|
|
713
|
+
|
|
714
|
+
const user = await db('users')
|
|
715
|
+
.where({ reset_token_hash: tokenHash })
|
|
716
|
+
.where('reset_token_expires', '>', new Date())
|
|
717
|
+
.first();
|
|
718
|
+
|
|
719
|
+
if (!user) {
|
|
720
|
+
return { error: 'Invalid or expired reset token' };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const hashedPassword = await bcrypt.hash(newPassword, 12);
|
|
724
|
+
|
|
725
|
+
await db('users').where({ id: user.id }).update({
|
|
726
|
+
password_hash: hashedPassword,
|
|
727
|
+
reset_token_hash: null,
|
|
728
|
+
reset_token_expires: null
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
return { success: true };
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
## Anti-Patterns
|
|
736
|
+
|
|
737
|
+
### 1. JWT in localStorage
|
|
738
|
+
|
|
739
|
+
```javascript
|
|
740
|
+
// BAD: Vulnerable to XSS
|
|
741
|
+
localStorage.setItem('token', jwtToken);
|
|
742
|
+
const token = localStorage.getItem('token');
|
|
743
|
+
|
|
744
|
+
// GOOD: httpOnly cookie
|
|
745
|
+
res.cookie('accessToken', jwtToken, {
|
|
746
|
+
httpOnly: true, // Not accessible via JavaScript
|
|
747
|
+
secure: true, // HTTPS only
|
|
748
|
+
sameSite: 'strict', // CSRF protection
|
|
749
|
+
maxAge: 900000 // 15 min
|
|
750
|
+
});
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
**Why**: localStorage is accessible via JavaScript, making tokens vulnerable to XSS attacks. httpOnly cookies cannot be accessed by client-side scripts.
|
|
754
|
+
|
|
755
|
+
### 2. Secrets in Code
|
|
756
|
+
|
|
757
|
+
```javascript
|
|
758
|
+
// BAD: Hardcoded secrets
|
|
759
|
+
const JWT_SECRET = 'my-secret-key-123';
|
|
760
|
+
const API_KEY = 'sk-1234567890abcdef';
|
|
761
|
+
|
|
762
|
+
app.listen(3000, () => {
|
|
763
|
+
console.log('JWT Secret:', JWT_SECRET); // Logged secret
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// GOOD: Environment variables
|
|
767
|
+
require('dotenv').config();
|
|
768
|
+
const JWT_SECRET = process.env.JWT_SECRET;
|
|
769
|
+
const API_KEY = process.env.API_KEY;
|
|
770
|
+
|
|
771
|
+
if (!JWT_SECRET || !API_KEY) {
|
|
772
|
+
throw new Error('Missing required environment variables');
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
**Why**: Hardcoded secrets are visible in version control and logs. Environment variables keep secrets out of code and allow different values per environment.
|
|
777
|
+
|
|
778
|
+
### 3. Client-Side Only Validation
|
|
779
|
+
|
|
780
|
+
```javascript
|
|
781
|
+
// BAD: Only client-side validation
|
|
782
|
+
// frontend.js
|
|
783
|
+
if (email.includes('@')) {
|
|
784
|
+
fetch('/api/users', {
|
|
785
|
+
method: 'POST',
|
|
786
|
+
body: JSON.stringify({ email })
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// backend.js
|
|
791
|
+
app.post('/api/users', (req, res) => {
|
|
792
|
+
const { email } = req.body;
|
|
793
|
+
db.createUser({ email }); // No validation - VULNERABLE
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// GOOD: Server-side validation (client-side is optional UX enhancement)
|
|
797
|
+
// backend.js
|
|
798
|
+
const emailSchema = z.string().email().max(255);
|
|
799
|
+
|
|
800
|
+
app.post('/api/users', (req, res) => {
|
|
801
|
+
const result = emailSchema.safeParse(req.body.email);
|
|
802
|
+
if (!result.success) {
|
|
803
|
+
return res.status(400).json({ error: 'Invalid email' });
|
|
804
|
+
}
|
|
805
|
+
db.createUser({ email: result.data });
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
**Why**: Client-side validation can be bypassed by sending direct HTTP requests. ALWAYS validate server-side.
|
|
810
|
+
|
|
811
|
+
### 4. Verbose Error Messages
|
|
812
|
+
|
|
813
|
+
```javascript
|
|
814
|
+
// BAD: Exposing internal details
|
|
815
|
+
app.use((err, req, res, next) => {
|
|
816
|
+
res.status(500).json({
|
|
817
|
+
error: err.message,
|
|
818
|
+
stack: err.stack,
|
|
819
|
+
sql: err.sql, // Exposes database schema
|
|
820
|
+
query: req.query, // May contain sensitive data
|
|
821
|
+
env: process.env // CRITICAL: Exposes all secrets
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// GOOD: Generic error in production
|
|
826
|
+
app.use((err, req, res, next) => {
|
|
827
|
+
// Log full error server-side
|
|
828
|
+
logger.error({
|
|
829
|
+
message: err.message,
|
|
830
|
+
stack: err.stack,
|
|
831
|
+
user: req.user?.id,
|
|
832
|
+
path: req.path
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Return generic error to client
|
|
836
|
+
const message = process.env.NODE_ENV === 'production'
|
|
837
|
+
? 'Internal server error'
|
|
838
|
+
: err.message;
|
|
839
|
+
|
|
840
|
+
res.status(500).json({ error: message });
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Why**: Detailed error messages leak sensitive information about your system architecture, database schema, and file structure. Attackers use this information to craft targeted attacks.
|
|
845
|
+
|
|
846
|
+
### 5. MD5 for Security
|
|
847
|
+
|
|
848
|
+
```javascript
|
|
849
|
+
// BAD: Using MD5 or SHA for passwords
|
|
850
|
+
const crypto = require('crypto');
|
|
851
|
+
const hashedPassword = crypto
|
|
852
|
+
.createHash('md5')
|
|
853
|
+
.update(password)
|
|
854
|
+
.digest('hex');
|
|
855
|
+
|
|
856
|
+
// Also BAD: SHA-256 (too fast for passwords)
|
|
857
|
+
const hashedPassword = crypto
|
|
858
|
+
.createHash('sha256')
|
|
859
|
+
.update(password)
|
|
860
|
+
.digest('hex');
|
|
861
|
+
|
|
862
|
+
// GOOD: bcrypt with proper cost factor
|
|
863
|
+
const bcrypt = require('bcrypt');
|
|
864
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
865
|
+
|
|
866
|
+
// Or argon2id
|
|
867
|
+
const argon2 = require('argon2');
|
|
868
|
+
const hashedPassword = await argon2.hash(password, {
|
|
869
|
+
type: argon2.argon2id,
|
|
870
|
+
memoryCost: 65536, // 64 MB
|
|
871
|
+
timeCost: 3,
|
|
872
|
+
parallelism: 4
|
|
873
|
+
});
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
**Why**: MD5 and SHA are designed for speed, making them vulnerable to brute-force attacks. Modern GPUs can compute billions of MD5 hashes per second. bcrypt and argon2id are designed to be slow and resistant to hardware acceleration.
|
|
877
|
+
|
|
878
|
+
## Integration Notes
|
|
879
|
+
|
|
880
|
+
### Workflow
|
|
881
|
+
|
|
882
|
+
1. **Before starting**: Call `team_check_inbox` to check for messages from orchestrator or other workers
|
|
883
|
+
|
|
884
|
+
2. **Read conventions**: Use the `artifact_read` tool to read the `shared-conventions` artifact for:
|
|
885
|
+
- Auth token format (JWT structure, claims)
|
|
886
|
+
- Error response format
|
|
887
|
+
- HTTP status codes
|
|
888
|
+
- Cookie configuration (names, flags)
|
|
889
|
+
- Rate limit values per endpoint type
|
|
890
|
+
|
|
891
|
+
3. **Publish security configuration**: Create a `security-config` artifact containing:
|
|
892
|
+
```json
|
|
893
|
+
{
|
|
894
|
+
"cors": {
|
|
895
|
+
"origins": ["https://app.example.com"],
|
|
896
|
+
"credentials": true,
|
|
897
|
+
"methods": ["GET", "POST", "PUT", "DELETE"]
|
|
898
|
+
},
|
|
899
|
+
"rateLimits": {
|
|
900
|
+
"auth": { "max": 5, "windowMs": 900000 },
|
|
901
|
+
"api": { "max": 100, "windowMs": 900000 },
|
|
902
|
+
"authenticated": { "max": 1000, "windowMs": 900000 }
|
|
903
|
+
},
|
|
904
|
+
"headers": {
|
|
905
|
+
"hsts": { "maxAge": 31536000, "includeSubDomains": true },
|
|
906
|
+
"csp": {
|
|
907
|
+
"defaultSrc": ["'self'"],
|
|
908
|
+
"scriptSrc": ["'self'"]
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
"auth": {
|
|
912
|
+
"accessTokenExpiry": "15m",
|
|
913
|
+
"refreshTokenExpiry": "7d",
|
|
914
|
+
"bcryptRounds": 12,
|
|
915
|
+
"cookieConfig": {
|
|
916
|
+
"httpOnly": true,
|
|
917
|
+
"secure": true,
|
|
918
|
+
"sameSite": "strict"
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
4. **Coordinate with other workers**:
|
|
925
|
+
- **Backend worker**: Use `team_ask` to confirm middleware integration points and error handling format
|
|
926
|
+
- **Frontend worker**: Use `team_ask` to confirm token storage strategy and CORS origins
|
|
927
|
+
- **Database worker**: Use `team_ask` to confirm user table schema includes password_hash, reset_token_hash, reset_token_expires columns
|
|
928
|
+
|
|
929
|
+
5. **Implementation order**:
|
|
930
|
+
- Set up Helmet and security headers first
|
|
931
|
+
- Implement authentication middleware
|
|
932
|
+
- Add authorization/RBAC
|
|
933
|
+
- Apply input validation schemas
|
|
934
|
+
- Configure rate limiting
|
|
935
|
+
- Set up CORS with proper origins
|
|
936
|
+
|
|
937
|
+
6. **File paths**: Use relative paths only (e.g., `./middleware/auth.js`, not `/app/middleware/auth.js`)
|
|
938
|
+
|
|
939
|
+
7. **Broadcast completion**: After publishing the security-config artifact, use `team_broadcast` to notify:
|
|
940
|
+
```javascript
|
|
941
|
+
{
|
|
942
|
+
from: "security",
|
|
943
|
+
content: "Security configuration published. All workers must consume security-config artifact for auth middleware, CORS, and headers."
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### Critical Dependencies
|
|
948
|
+
|
|
949
|
+
All workers MUST consume the `security-config` artifact before implementing:
|
|
950
|
+
- API routes (need auth middleware and rate limiting)
|
|
951
|
+
- Frontend API calls (need CORS origins and token handling)
|
|
952
|
+
- Database operations (need input validation to prevent SQL injection)
|
|
953
|
+
- Error handlers (need proper error format without leaking details)
|
|
954
|
+
|
|
955
|
+
### Environment Variables Required
|
|
956
|
+
|
|
957
|
+
Ensure these are in `.env.example`:
|
|
958
|
+
```bash
|
|
959
|
+
# JWT
|
|
960
|
+
JWT_SECRET=
|
|
961
|
+
JWT_REFRESH_SECRET=
|
|
962
|
+
|
|
963
|
+
# Database
|
|
964
|
+
DATABASE_URL=
|
|
965
|
+
|
|
966
|
+
# CORS
|
|
967
|
+
ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
|
|
968
|
+
|
|
969
|
+
# Rate Limiting
|
|
970
|
+
RATE_LIMIT_WINDOW_MS=900000
|
|
971
|
+
RATE_LIMIT_MAX_REQUESTS=100
|
|
972
|
+
|
|
973
|
+
# Environment
|
|
974
|
+
NODE_ENV=development
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### Testing Security
|
|
978
|
+
|
|
979
|
+
Before marking security implementation as complete:
|
|
980
|
+
1. Verify all endpoints require authentication
|
|
981
|
+
2. Test RBAC with different roles
|
|
982
|
+
3. Attempt SQL injection with `' OR '1'='1` payloads
|
|
983
|
+
4. Verify CORS blocks unauthorized origins
|
|
984
|
+
5. Test rate limiting with rapid requests
|
|
985
|
+
6. Confirm secrets are not in git history: `git log -p | grep -i "password\|secret\|api_key"`
|
|
986
|
+
|
|
987
|
+
### Security Checklist
|
|
988
|
+
|
|
989
|
+
- [ ] All passwords hashed with bcrypt (cost ≥12)
|
|
990
|
+
- [ ] JWT in httpOnly cookies, NOT localStorage
|
|
991
|
+
- [ ] All SQL queries parameterized
|
|
992
|
+
- [ ] Input validation on ALL endpoints
|
|
993
|
+
- [ ] Rate limiting on auth endpoints (5/15min)
|
|
994
|
+
- [ ] CORS whitelist (no `*` with credentials)
|
|
995
|
+
- [ ] HSTS header configured
|
|
996
|
+
- [ ] CSP header blocks inline scripts
|
|
997
|
+
- [ ] Secrets in .env, NOT in code
|
|
998
|
+
- [ ] .env in .gitignore
|
|
999
|
+
- [ ] Error messages don't expose stack traces in production
|
|
1000
|
+
- [ ] HTTPS enforced via redirect
|