@schalkneethling/toolkit 0.1.5 → 0.3.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.
Files changed (30) hide show
  1. package/README.md +29 -7
  2. package/dist/index.mjs +90 -6
  3. package/dist/index.mjs.map +1 -1
  4. package/hooks/auto-approve-safe-commands/hook.mjs +134 -0
  5. package/hooks/auto-approve-safe-commands/hook.mts +188 -0
  6. package/hooks/auto-approve-safe-commands/settings-fragment.json +17 -0
  7. package/hooks/block-dangerous-commands/hook.mjs +3 -3
  8. package/hooks/block-dangerous-commands/hook.mts +23 -10
  9. package/package.json +8 -10
  10. package/skills/css-coder/SKILL.md +95 -0
  11. package/skills/css-coder/references/patterns.md +224 -0
  12. package/skills/css-tokens/README.md +152 -0
  13. package/skills/css-tokens/SKILL.md +125 -0
  14. package/skills/css-tokens/references/tokens.css +162 -0
  15. package/skills/frontend-security/SKILL.md +134 -0
  16. package/skills/frontend-security/references/csp-configuration.md +191 -0
  17. package/skills/frontend-security/references/csrf-protection.md +327 -0
  18. package/skills/frontend-security/references/dom-security.md +229 -0
  19. package/skills/frontend-security/references/file-upload-security.md +310 -0
  20. package/skills/frontend-security/references/framework-patterns.md +307 -0
  21. package/skills/frontend-security/references/input-validation.md +232 -0
  22. package/skills/frontend-security/references/jwt-security.md +300 -0
  23. package/skills/frontend-security/references/nodejs-npm-security.md +261 -0
  24. package/skills/frontend-security/references/xss-prevention.md +163 -0
  25. package/skills/frontend-testing/SKILL.md +357 -0
  26. package/skills/frontend-testing/references/accessibility-testing.md +368 -0
  27. package/skills/frontend-testing/references/aria-snapshots.md +517 -0
  28. package/skills/frontend-testing/references/locator-strategies.md +295 -0
  29. package/skills/frontend-testing/references/visual-regression.md +466 -0
  30. 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