@schalkneethling/toolkit 0.2.0 → 0.4.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 +29 -7
- package/commands/rpm-advance.md +13 -0
- package/commands/rpm-checkpoint.md +13 -0
- package/commands/rpm-feedback.md +15 -0
- package/commands/rpm-handoff.md +14 -0
- package/commands/rpm-review.md +13 -0
- package/commands/rpm-start.md +13 -0
- package/dist/index.mjs +90 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/skills/css-coder/SKILL.md +95 -0
- package/skills/css-coder/references/patterns.md +224 -0
- package/skills/css-tokens/README.md +152 -0
- package/skills/css-tokens/SKILL.md +125 -0
- package/skills/css-tokens/references/tokens.css +162 -0
- package/skills/frontend-security/SKILL.md +134 -0
- package/skills/frontend-security/references/csp-configuration.md +191 -0
- package/skills/frontend-security/references/csrf-protection.md +327 -0
- package/skills/frontend-security/references/dom-security.md +229 -0
- package/skills/frontend-security/references/file-upload-security.md +310 -0
- package/skills/frontend-security/references/framework-patterns.md +307 -0
- package/skills/frontend-security/references/input-validation.md +232 -0
- package/skills/frontend-security/references/jwt-security.md +300 -0
- package/skills/frontend-security/references/nodejs-npm-security.md +261 -0
- package/skills/frontend-security/references/xss-prevention.md +163 -0
- package/skills/frontend-testing/SKILL.md +357 -0
- package/skills/frontend-testing/references/accessibility-testing.md +368 -0
- package/skills/frontend-testing/references/aria-snapshots.md +517 -0
- package/skills/frontend-testing/references/locator-strategies.md +295 -0
- package/skills/frontend-testing/references/visual-regression.md +466 -0
- package/skills/refined-plan-mode/SKILL.md +84 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# JWT Security Reference
|
|
2
|
+
|
|
3
|
+
## Common Vulnerabilities
|
|
4
|
+
|
|
5
|
+
### None Algorithm Attack
|
|
6
|
+
|
|
7
|
+
Attack: Attacker changes algorithm to "none" to bypass signature verification.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
// VULNERABLE - accepts any algorithm
|
|
11
|
+
const decoded = jwt.verify(token, secret);
|
|
12
|
+
|
|
13
|
+
// SECURE - explicitly specify allowed algorithms
|
|
14
|
+
const decoded = jwt.verify(token, secret, {
|
|
15
|
+
algorithms: ['HS256'] // Only accept HS256
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Algorithm Confusion Attack
|
|
20
|
+
|
|
21
|
+
Attack: Switching from RS256 to HS256 using public key as secret.
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
// VULNERABLE - auto-detects algorithm
|
|
25
|
+
const decoded = jwt.verify(token, publicKey);
|
|
26
|
+
|
|
27
|
+
// SECURE - specify expected algorithm
|
|
28
|
+
const decoded = jwt.verify(token, publicKey, {
|
|
29
|
+
algorithms: ['RS256'] // Only accept RS256
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Weak Secret
|
|
34
|
+
|
|
35
|
+
Attack: Brute-force weak HMAC secrets.
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
// VULNERABLE - weak secret
|
|
39
|
+
const token = jwt.sign(payload, 'password123');
|
|
40
|
+
|
|
41
|
+
// SECURE - strong random secret (256+ bits)
|
|
42
|
+
const crypto = require('crypto');
|
|
43
|
+
const secret = crypto.randomBytes(32); // 256 bits
|
|
44
|
+
const token = jwt.sign(payload, secret);
|
|
45
|
+
|
|
46
|
+
// Or use RSA keys
|
|
47
|
+
const privateKey = fs.readFileSync('private.pem');
|
|
48
|
+
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Token Sidejacking Prevention
|
|
52
|
+
|
|
53
|
+
Add fingerprint to prevent stolen token reuse:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
const crypto = require('crypto');
|
|
57
|
+
|
|
58
|
+
// Generate fingerprint on login
|
|
59
|
+
function generateFingerprint() {
|
|
60
|
+
return crypto.randomBytes(32).toString('hex');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create token with fingerprint hash
|
|
64
|
+
function createToken(userId, fingerprint) {
|
|
65
|
+
const fingerprintHash = crypto
|
|
66
|
+
.createHash('sha256')
|
|
67
|
+
.update(fingerprint)
|
|
68
|
+
.digest('hex');
|
|
69
|
+
|
|
70
|
+
return jwt.sign(
|
|
71
|
+
{ sub: userId, fph: fingerprintHash },
|
|
72
|
+
secret,
|
|
73
|
+
{ expiresIn: '15m' }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Set fingerprint as httpOnly cookie
|
|
78
|
+
res.cookie('__Secure-Fgp', fingerprint, {
|
|
79
|
+
httpOnly: true,
|
|
80
|
+
secure: true,
|
|
81
|
+
sameSite: 'Strict',
|
|
82
|
+
maxAge: 15 * 60 * 1000 // Match token expiry
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Validate both token and fingerprint
|
|
86
|
+
function validateToken(token, fingerprintCookie) {
|
|
87
|
+
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
|
|
88
|
+
|
|
89
|
+
const fingerprintHash = crypto
|
|
90
|
+
.createHash('sha256')
|
|
91
|
+
.update(fingerprintCookie)
|
|
92
|
+
.digest('hex');
|
|
93
|
+
|
|
94
|
+
if (decoded.fph !== fingerprintHash) {
|
|
95
|
+
throw new Error('Invalid fingerprint');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return decoded;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Token Storage (Client-Side)
|
|
103
|
+
|
|
104
|
+
### Recommended: sessionStorage + Fingerprint Cookie
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
// Store token in sessionStorage
|
|
108
|
+
sessionStorage.setItem('token', jwt);
|
|
109
|
+
|
|
110
|
+
// Clear on logout
|
|
111
|
+
sessionStorage.removeItem('token');
|
|
112
|
+
|
|
113
|
+
// Send in Authorization header
|
|
114
|
+
fetch('/api/data', {
|
|
115
|
+
headers: {
|
|
116
|
+
'Authorization': `Bearer ${sessionStorage.getItem('token')}`
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Alternative: httpOnly Cookie
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
// Server sets cookie
|
|
125
|
+
res.cookie('token', jwt, {
|
|
126
|
+
httpOnly: true, // Not accessible via JavaScript
|
|
127
|
+
secure: true, // HTTPS only
|
|
128
|
+
sameSite: 'Strict', // CSRF protection
|
|
129
|
+
maxAge: 15 * 60 * 1000
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Need CSRF protection with cookie-based auth
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Avoid: localStorage
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// VULNERABLE - persists after browser close, accessible to XSS
|
|
139
|
+
localStorage.setItem('token', jwt); // Not recommended
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Token Expiration
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
// Short-lived access tokens (15-60 minutes)
|
|
146
|
+
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
|
|
147
|
+
|
|
148
|
+
// Longer refresh tokens (days/weeks)
|
|
149
|
+
const refreshToken = jwt.sign(
|
|
150
|
+
{ sub: userId, type: 'refresh' },
|
|
151
|
+
refreshSecret,
|
|
152
|
+
{ expiresIn: '7d' }
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Refresh endpoint
|
|
156
|
+
app.post('/refresh', async (req, res) => {
|
|
157
|
+
const { refreshToken } = req.body;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const decoded = jwt.verify(refreshToken, refreshSecret, {
|
|
161
|
+
algorithms: ['HS256']
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (decoded.type !== 'refresh') {
|
|
165
|
+
throw new Error('Invalid token type');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if refresh token is revoked
|
|
169
|
+
if (await isTokenRevoked(decoded)) {
|
|
170
|
+
throw new Error('Token revoked');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Issue new access token
|
|
174
|
+
const newAccessToken = jwt.sign(
|
|
175
|
+
{ sub: decoded.sub },
|
|
176
|
+
secret,
|
|
177
|
+
{ expiresIn: '15m' }
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
res.json({ accessToken: newAccessToken });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
res.status(401).json({ error: 'Invalid refresh token' });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Token Revocation
|
|
188
|
+
|
|
189
|
+
JWTs are stateless, so revocation requires additional mechanisms:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
// Denylist approach
|
|
193
|
+
const revokedTokens = new Map(); // Use Redis in production
|
|
194
|
+
|
|
195
|
+
function revokeToken(token) {
|
|
196
|
+
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
|
|
197
|
+
const tokenId = decoded.jti;
|
|
198
|
+
const expiry = decoded.exp * 1000;
|
|
199
|
+
|
|
200
|
+
if (!tokenId || !expiry) {
|
|
201
|
+
throw new Error('Token missing required revocation claims');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Store until token expires
|
|
205
|
+
revokedTokens.set(tokenId, expiry);
|
|
206
|
+
|
|
207
|
+
// Clean up expired entries periodically
|
|
208
|
+
setTimeout(() => revokedTokens.delete(tokenId), Math.max(0, expiry - Date.now()));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isTokenRevoked(decoded) {
|
|
212
|
+
return Boolean(decoded.jti) && revokedTokens.has(decoded.jti);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Include jti (JWT ID) in tokens
|
|
216
|
+
const token = jwt.sign(
|
|
217
|
+
{ sub: userId, jti: crypto.randomUUID() },
|
|
218
|
+
secret,
|
|
219
|
+
{ expiresIn: '15m' }
|
|
220
|
+
);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Token Information Disclosure
|
|
224
|
+
|
|
225
|
+
JWTs are base64-encoded, not encrypted. Sensitive data is visible.
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
// VULNERABLE - sensitive data in payload
|
|
229
|
+
const token = jwt.sign({
|
|
230
|
+
sub: userId,
|
|
231
|
+
ssn: '123-45-6789', // Visible to anyone!
|
|
232
|
+
salary: 100000
|
|
233
|
+
}, secret);
|
|
234
|
+
|
|
235
|
+
// SECURE - minimal claims
|
|
236
|
+
const token = jwt.sign({
|
|
237
|
+
sub: userId,
|
|
238
|
+
role: 'user'
|
|
239
|
+
}, secret);
|
|
240
|
+
|
|
241
|
+
// If sensitive data required, encrypt the token
|
|
242
|
+
const encryptedToken = encrypt(token, encryptionKey);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Validation Middleware
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
function authenticateToken(req, res, next) {
|
|
249
|
+
// Get token from header
|
|
250
|
+
const authHeader = req.headers.authorization;
|
|
251
|
+
const token = authHeader?.split(' ')[1]; // "Bearer TOKEN"
|
|
252
|
+
|
|
253
|
+
if (!token) {
|
|
254
|
+
return res.status(401).json({ error: 'Token required' });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Verify with explicit algorithm
|
|
259
|
+
const decoded = jwt.verify(token, secret, {
|
|
260
|
+
algorithms: ['HS256'],
|
|
261
|
+
issuer: 'myapp',
|
|
262
|
+
audience: 'myapp-users'
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Validate fingerprint if using sidejacking protection
|
|
266
|
+
const fingerprint = req.cookies['__Secure-Fgp'];
|
|
267
|
+
if (!validateFingerprint(decoded, fingerprint)) {
|
|
268
|
+
throw new Error('Invalid fingerprint');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check revocation
|
|
272
|
+
if (isTokenRevoked(decoded)) {
|
|
273
|
+
throw new Error('Token revoked');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
req.user = decoded;
|
|
277
|
+
next();
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error.name === 'TokenExpiredError') {
|
|
280
|
+
return res.status(401).json({ error: 'Token expired' });
|
|
281
|
+
}
|
|
282
|
+
return res.status(403).json({ error: 'Invalid token' });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Security Checklist
|
|
288
|
+
|
|
289
|
+
- [ ] Explicit algorithm specification (never auto-detect)
|
|
290
|
+
- [ ] Strong secret (256+ bits) or RSA keys
|
|
291
|
+
- [ ] Short expiration times (15-60 minutes for access tokens)
|
|
292
|
+
- [ ] Token fingerprint with httpOnly cookie
|
|
293
|
+
- [ ] Validate issuer (iss) and audience (aud) claims
|
|
294
|
+
- [ ] Implement token revocation mechanism
|
|
295
|
+
- [ ] No sensitive data in payload
|
|
296
|
+
- [ ] Store in sessionStorage (not localStorage)
|
|
297
|
+
- [ ] Send via Authorization header
|
|
298
|
+
- [ ] Use HTTPS only
|
|
299
|
+
|
|
300
|
+
OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Node.js and NPM Security Reference
|
|
2
|
+
|
|
3
|
+
## NPM Dependency Security
|
|
4
|
+
|
|
5
|
+
### Audit Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Check for vulnerabilities
|
|
9
|
+
npm audit
|
|
10
|
+
|
|
11
|
+
# Fix automatically where possible
|
|
12
|
+
npm audit fix
|
|
13
|
+
|
|
14
|
+
# Force fix (may have breaking changes)
|
|
15
|
+
npm audit fix --force
|
|
16
|
+
|
|
17
|
+
# Generate detailed report
|
|
18
|
+
npm audit --json > audit-report.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Lockfile Enforcement
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Always use lockfile in CI/CD
|
|
25
|
+
npm ci # Instead of npm install
|
|
26
|
+
|
|
27
|
+
# Verify lockfile integrity
|
|
28
|
+
npm ci --ignore-scripts # Safer for first run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Package.json Security
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"scripts": {
|
|
36
|
+
"preinstall": "npx npm-force-resolutions",
|
|
37
|
+
"postinstall": "npm audit"
|
|
38
|
+
},
|
|
39
|
+
"overrides": {
|
|
40
|
+
"vulnerable-package": "^2.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Dangerous Functions
|
|
46
|
+
|
|
47
|
+
### Code Execution
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// DANGEROUS - never use with user input
|
|
51
|
+
eval(userInput);
|
|
52
|
+
new Function(userInput);
|
|
53
|
+
vm.runInThisContext(userInput);
|
|
54
|
+
require(userInput);
|
|
55
|
+
|
|
56
|
+
// DANGEROUS - setTimeout/setInterval with strings
|
|
57
|
+
setTimeout(userInput, 1000); // Executes as code
|
|
58
|
+
|
|
59
|
+
// SAFE - pass functions instead
|
|
60
|
+
setTimeout(() => { /* code */ }, 1000);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Child Process Injection
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// DANGEROUS - command injection
|
|
67
|
+
const { exec } = require('child_process');
|
|
68
|
+
exec(`ls ${userInput}`); // Shell injection
|
|
69
|
+
|
|
70
|
+
// SAFER - use execFile with arguments array
|
|
71
|
+
const { execFile } = require('child_process');
|
|
72
|
+
execFile('ls', [userInput], callback); // Arguments not interpreted by shell
|
|
73
|
+
|
|
74
|
+
// SAFEST - use spawn with shell: false
|
|
75
|
+
const { spawn } = require('child_process');
|
|
76
|
+
spawn('ls', [userInput], { shell: false });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### File System
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
const path = require('path');
|
|
83
|
+
const fs = require('fs');
|
|
84
|
+
|
|
85
|
+
// DANGEROUS - path traversal
|
|
86
|
+
const filePath = `/uploads/${userInput}`;
|
|
87
|
+
|
|
88
|
+
// SAFE - validate and resolve path
|
|
89
|
+
function safeReadFile(userInput, baseDir) {
|
|
90
|
+
const basePath = path.resolve(baseDir);
|
|
91
|
+
const safePath = path.resolve(basePath, userInput);
|
|
92
|
+
const relativePath = path.relative(basePath, safePath);
|
|
93
|
+
|
|
94
|
+
// Verify path is within allowed directory
|
|
95
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
96
|
+
throw new Error('Invalid file path');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return fs.readFileSync(safePath);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Request Handling
|
|
104
|
+
|
|
105
|
+
### Rate Limiting
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
const rateLimit = require('express-rate-limit');
|
|
109
|
+
|
|
110
|
+
const limiter = rateLimit({
|
|
111
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
112
|
+
max: 100, // Limit each IP to 100 requests per window
|
|
113
|
+
message: 'Too many requests',
|
|
114
|
+
standardHeaders: true,
|
|
115
|
+
legacyHeaders: false
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
app.use(limiter);
|
|
119
|
+
|
|
120
|
+
// Stricter limits for auth endpoints
|
|
121
|
+
const authLimiter = rateLimit({
|
|
122
|
+
windowMs: 60 * 60 * 1000, // 1 hour
|
|
123
|
+
max: 5, // 5 attempts per hour
|
|
124
|
+
message: 'Too many login attempts'
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
app.use('/api/login', authLimiter);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Request Size Limits
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
const express = require('express');
|
|
134
|
+
const app = express();
|
|
135
|
+
|
|
136
|
+
// Limit JSON body size
|
|
137
|
+
app.use(express.json({ limit: '100kb' }));
|
|
138
|
+
|
|
139
|
+
// Limit URL-encoded body
|
|
140
|
+
app.use(express.urlencoded({ extended: true, limit: '100kb' }));
|
|
141
|
+
|
|
142
|
+
// Limit file uploads
|
|
143
|
+
const multer = require('multer');
|
|
144
|
+
const upload = multer({
|
|
145
|
+
limits: { fileSize: 5 * 1024 * 1024 } // 5MB
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Timeout Configuration
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
const server = app.listen(3000);
|
|
153
|
+
|
|
154
|
+
// Set timeouts
|
|
155
|
+
server.setTimeout(30000); // 30 seconds
|
|
156
|
+
server.keepAliveTimeout = 65000;
|
|
157
|
+
server.headersTimeout = 66000;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Secure Headers
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const helmet = require('helmet');
|
|
164
|
+
|
|
165
|
+
app.use(helmet({
|
|
166
|
+
contentSecurityPolicy: {
|
|
167
|
+
directives: {
|
|
168
|
+
defaultSrc: ["'self'"],
|
|
169
|
+
scriptSrc: ["'self'"],
|
|
170
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
171
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
172
|
+
objectSrc: ["'none'"],
|
|
173
|
+
upgradeInsecureRequests: []
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
hsts: {
|
|
177
|
+
maxAge: 31536000,
|
|
178
|
+
includeSubDomains: true,
|
|
179
|
+
preload: true
|
|
180
|
+
},
|
|
181
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
182
|
+
frameguard: { action: 'deny' }
|
|
183
|
+
}));
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Error Handling
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
// Global error handler - don't expose details
|
|
190
|
+
app.use((err, req, res, next) => {
|
|
191
|
+
// Log full error internally
|
|
192
|
+
console.error(err);
|
|
193
|
+
|
|
194
|
+
// Send generic message to client
|
|
195
|
+
res.status(500).json({
|
|
196
|
+
error: 'An unexpected error occurred'
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Async error wrapper
|
|
201
|
+
const asyncHandler = fn => (req, res, next) => {
|
|
202
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
app.get('/data', asyncHandler(async (req, res) => {
|
|
206
|
+
const data = await fetchData();
|
|
207
|
+
res.json(data);
|
|
208
|
+
}));
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Environment Variables
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
// NEVER commit secrets to code
|
|
215
|
+
// Use environment variables
|
|
216
|
+
const apiKey = process.env.API_KEY;
|
|
217
|
+
|
|
218
|
+
// Validate required env vars at startup
|
|
219
|
+
const required = ['API_KEY', 'DB_URL', 'SESSION_SECRET'];
|
|
220
|
+
required.forEach(varName => {
|
|
221
|
+
if (!process.env[varName]) {
|
|
222
|
+
console.error(`Missing required env var: ${varName}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Regex DoS Prevention
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// DANGEROUS - evil regex (catastrophic backtracking)
|
|
232
|
+
const evilRegex = /^(a+)+$/;
|
|
233
|
+
evilRegex.test('aaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // Hangs
|
|
234
|
+
|
|
235
|
+
// Heuristic only: safe-regex can have false positives/negatives for ReDoS
|
|
236
|
+
const safe = require('safe-regex');
|
|
237
|
+
if (!safe(userProvidedRegex)) {
|
|
238
|
+
throw new Error('Unsafe regex pattern');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Preferred for untrusted patterns: use RE2 for guaranteed linear time
|
|
242
|
+
const RE2 = require('re2');
|
|
243
|
+
const pattern = new RE2(userProvidedRegex);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## NPM Security Checklist
|
|
247
|
+
|
|
248
|
+
- [ ] Run `npm audit` regularly and in CI/CD
|
|
249
|
+
- [ ] Use `npm ci` instead of `npm install` in CI
|
|
250
|
+
- [ ] Enable 2FA on npm account
|
|
251
|
+
- [ ] Use lockfiles and commit them
|
|
252
|
+
- [ ] Review new dependencies before installation
|
|
253
|
+
- [ ] Use `--ignore-scripts` for untrusted packages
|
|
254
|
+
- [ ] Set up automated vulnerability scanning (Snyk, Dependabot)
|
|
255
|
+
- [ ] Keep dependencies updated
|
|
256
|
+
- [ ] Avoid typosquatting by double-checking package names
|
|
257
|
+
- [ ] Use `npm-shrinkwrap.json` for published packages
|
|
258
|
+
|
|
259
|
+
OWASP References:
|
|
260
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html
|
|
261
|
+
- https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# XSS Prevention Reference
|
|
2
|
+
|
|
3
|
+
## Output Encoding Rules
|
|
4
|
+
|
|
5
|
+
Apply context-appropriate encoding for all untrusted data:
|
|
6
|
+
|
|
7
|
+
| Context | Encoding Method | Example |
|
|
8
|
+
|---------|-----------------|---------|
|
|
9
|
+
| HTML Body | HTML Entity Encoding | `<script>` |
|
|
10
|
+
| HTML Attribute | Attribute Encoding | `"onclick"` |
|
|
11
|
+
| JavaScript | JavaScript Encoding | `\x3cscript\x3e` |
|
|
12
|
+
| CSS | CSS Encoding | `\3c script\3e` |
|
|
13
|
+
| URL Parameter | URL Encoding | `%3Cscript%3E` |
|
|
14
|
+
|
|
15
|
+
## Safe vs Unsafe Sinks
|
|
16
|
+
|
|
17
|
+
### Unsafe Sinks (Never use with untrusted data)
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// Execution sinks - NEVER use with user input
|
|
21
|
+
element.innerHTML = userInput; // XSS
|
|
22
|
+
element.outerHTML = userInput; // XSS
|
|
23
|
+
document.write(userInput); // XSS
|
|
24
|
+
document.writeln(userInput); // XSS
|
|
25
|
+
|
|
26
|
+
// JavaScript execution sinks
|
|
27
|
+
eval(userInput); // XSS
|
|
28
|
+
new Function(userInput); // XSS
|
|
29
|
+
setTimeout(userInput, time); // XSS if string
|
|
30
|
+
setInterval(userInput, time); // XSS if string
|
|
31
|
+
|
|
32
|
+
// URL sinks
|
|
33
|
+
location.href = userInput; // XSS
|
|
34
|
+
location.assign(userInput); // XSS
|
|
35
|
+
location.replace(userInput); // XSS
|
|
36
|
+
window.open(userInput); // XSS
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Safe Alternatives
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// Safe text insertion
|
|
43
|
+
element.textContent = userInput; // Safe
|
|
44
|
+
element.innerText = userInput; // Safe
|
|
45
|
+
|
|
46
|
+
// Safe attribute setting (for safe attributes)
|
|
47
|
+
element.setAttribute('title', userInput); // Safe for non-event attributes
|
|
48
|
+
|
|
49
|
+
// Safe URL handling
|
|
50
|
+
const url = new URL(userInput, window.location.origin);
|
|
51
|
+
if (url.protocol === 'https:') {
|
|
52
|
+
location.href = url.href;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## HTML Sanitization
|
|
57
|
+
|
|
58
|
+
When HTML must be rendered, use sanitization:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
// Using DOMPurify (recommended)
|
|
62
|
+
import DOMPurify from 'dompurify';
|
|
63
|
+
element.innerHTML = DOMPurify.sanitize(userInput);
|
|
64
|
+
|
|
65
|
+
// With configuration
|
|
66
|
+
const clean = DOMPurify.sanitize(dirty, {
|
|
67
|
+
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
|
|
68
|
+
ALLOWED_ATTR: ['href', 'title']
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Browser Sanitizer API (when available)
|
|
72
|
+
const sanitizer = new Sanitizer({
|
|
73
|
+
allowElements: ['b', 'i', 'em', 'strong', 'a'],
|
|
74
|
+
allowAttributes: { 'href': ['a'] }
|
|
75
|
+
});
|
|
76
|
+
element.setHTML(userInput, { sanitizer });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Framework-Specific XSS Risks
|
|
80
|
+
|
|
81
|
+
### React
|
|
82
|
+
|
|
83
|
+
```jsx
|
|
84
|
+
// DANGEROUS - bypasses React's protection
|
|
85
|
+
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
|
86
|
+
|
|
87
|
+
// SAFE - React auto-escapes
|
|
88
|
+
<div>{userInput}</div>
|
|
89
|
+
|
|
90
|
+
// DANGEROUS - href with javascript:
|
|
91
|
+
<a href={userInput}>Link</a> // If userInput = "javascript:alert(1)"
|
|
92
|
+
|
|
93
|
+
// SAFE - validate URL protocol
|
|
94
|
+
const safeUrl = userInput.startsWith('https://') ? userInput : '#';
|
|
95
|
+
<a href={safeUrl}>Link</a>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Twig
|
|
99
|
+
|
|
100
|
+
```twig
|
|
101
|
+
{# DANGEROUS - raw filter bypasses escaping #}
|
|
102
|
+
{{ userInput|raw }}
|
|
103
|
+
|
|
104
|
+
{# DANGEROUS - autoescape disabled #}
|
|
105
|
+
{% autoescape false %}
|
|
106
|
+
{{ userInput }}
|
|
107
|
+
{% endautoescape %}
|
|
108
|
+
|
|
109
|
+
{# SAFE - auto-escaped by default #}
|
|
110
|
+
{{ userInput }}
|
|
111
|
+
|
|
112
|
+
{# SAFE - explicit escaping #}
|
|
113
|
+
{{ userInput|e('html') }}
|
|
114
|
+
{{ userInput|e('js') }}
|
|
115
|
+
{{ userInput|e('url') }}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Astro
|
|
119
|
+
|
|
120
|
+
```astro
|
|
121
|
+
<!-- DANGEROUS - set:html bypasses escaping -->
|
|
122
|
+
<div set:html={userInput} />
|
|
123
|
+
|
|
124
|
+
<!-- SAFE - auto-escaped -->
|
|
125
|
+
<div>{userInput}</div>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## URL Validation
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
function isValidUrl(input) {
|
|
132
|
+
try {
|
|
133
|
+
const url = new URL(input);
|
|
134
|
+
return ['http:', 'https:'].includes(url.protocol);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prevent javascript: URLs
|
|
141
|
+
function sanitizeHref(input) {
|
|
142
|
+
if (!input) return '#';
|
|
143
|
+
const trimmed = input.trim().toLowerCase();
|
|
144
|
+
if (trimmed.startsWith('javascript:') ||
|
|
145
|
+
trimmed.startsWith('data:') ||
|
|
146
|
+
trimmed.startsWith('vbscript:')) {
|
|
147
|
+
return '#';
|
|
148
|
+
}
|
|
149
|
+
return input;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Content-Type Headers
|
|
154
|
+
|
|
155
|
+
Always set appropriate Content-Type headers:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
// Express.js
|
|
159
|
+
res.setHeader('Content-Type', 'application/json');
|
|
160
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
|