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.
@@ -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