@schalkneethling/toolkit 0.2.0 → 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.
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: frontend-security
3
+ description: Audit frontend codebases for security vulnerabilities and bad practices. Use when performing security reviews, auditing code for XSS/CSRF/DOM vulnerabilities, checking Content Security Policy configurations, validating input handling, reviewing file upload security, or examining Node.js/NPM dependencies. Target frameworks include web platform (vanilla HTML/CSS/JS), React, Astro, Twig templates, Node.js, and Bun. Based on OWASP security guidelines.
4
+ ---
5
+
6
+ # Frontend Security Audit Skill
7
+
8
+ Perform comprehensive security audits of frontend codebases to identify vulnerabilities, bad practices, and missing protections.
9
+
10
+ ## Audit Process
11
+
12
+ 1. **Scan for dangerous patterns** - Search codebase for known vulnerability indicators
13
+ 2. **Review framework-specific risks** - Check for framework security bypass patterns
14
+ 3. **Validate defensive measures** - Verify CSP, CSRF tokens, input validation
15
+ 4. **Check dependencies** - Review npm/node dependencies for vulnerabilities
16
+ 5. **Report findings** - Categorize by severity with remediation guidance
17
+
18
+ ## Critical Vulnerability Patterns to Search
19
+
20
+ ### XSS Indicators (Search Priority: HIGH)
21
+
22
+ ```bash
23
+ # React dangerous patterns
24
+ grep -rn "dangerouslySetInnerHTML" --include="*.jsx" --include="*.tsx" --include="*.js"
25
+
26
+ # Direct DOM manipulation
27
+ grep -rn "\.innerHTML\s*=" --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx"
28
+ grep -rn "\.outerHTML\s*=" --include="*.js" --include="*.ts"
29
+ grep -rn "document\.write" --include="*.js" --include="*.ts"
30
+
31
+ # URL-based injection
32
+ grep -rn "location\.href\s*=" --include="*.js" --include="*.ts"
33
+ grep -rn "location\.replace" --include="*.js" --include="*.ts"
34
+ grep -rn "window\.open" --include="*.js" --include="*.ts"
35
+
36
+ # Eval and code execution
37
+ grep -rn "eval\s*(" --include="*.js" --include="*.ts"
38
+ grep -rn "new Function\s*(" --include="*.js" --include="*.ts"
39
+ grep -rn "setTimeout\s*(\s*['\"]" --include="*.js" --include="*.ts"
40
+ grep -rn "setInterval\s*(\s*['\"]" --include="*.js" --include="*.ts"
41
+
42
+ # Twig unescaped output
43
+ grep -rn "|raw" --include="*.twig" --include="*.html.twig"
44
+ grep -rn "{% autoescape false %}" --include="*.twig"
45
+ ```
46
+
47
+ ### CSRF Indicators
48
+
49
+ ```bash
50
+ # Forms without CSRF tokens
51
+ grep -rn "<form" --include="*.html" --include="*.jsx" --include="*.tsx" --include="*.twig"
52
+
53
+ # State-changing requests without protection
54
+ grep -rn "fetch\s*(" --include="*.js" --include="*.ts" | grep -E "(POST|PUT|DELETE|PATCH)"
55
+ grep -rn "axios\.(post|put|delete|patch)" --include="*.js" --include="*.ts"
56
+ ```
57
+
58
+ ### Sensitive Data Exposure
59
+
60
+ ```bash
61
+ # localStorage/sessionStorage with sensitive data
62
+ grep -rn "localStorage\." --include="*.js" --include="*.ts"
63
+ grep -rn "sessionStorage\." --include="*.js" --include="*.ts"
64
+
65
+ # Hardcoded secrets
66
+ grep -rn "api[_-]?key\s*[:=]" --include="*.js" --include="*.ts" --include="*.env"
67
+ grep -rn "secret\s*[:=]" --include="*.js" --include="*.ts"
68
+ grep -rn "password\s*[:=]" --include="*.js" --include="*.ts"
69
+ ```
70
+
71
+ ## Reference Documentation
72
+
73
+ Load these references based on findings:
74
+
75
+ - **XSS vulnerabilities found**: See `references/xss-prevention.md`
76
+ - **CSRF concerns**: See `references/csrf-protection.md`
77
+ - **DOM manipulation issues**: See `references/dom-security.md`
78
+ - **CSP review needed**: See `references/csp-configuration.md`
79
+ - **Input handling issues**: See `references/input-validation.md`
80
+ - **Node.js/NPM audit**: See `references/nodejs-npm-security.md`
81
+ - **Framework-specific patterns**: See `references/framework-patterns.md`
82
+ - **File upload handling**: See `references/file-upload-security.md`
83
+ - **JWT implementation**: See `references/jwt-security.md`
84
+
85
+ ## Severity Classification
86
+
87
+ **CRITICAL** - Exploitable XSS, authentication bypass, secrets exposure
88
+ **HIGH** - Missing CSRF protection, unsafe DOM manipulation, SQL injection vectors
89
+ **MEDIUM** - Weak CSP, missing security headers, improper input validation
90
+ **LOW** - Informational disclosure, deprecated functions, suboptimal practices
91
+
92
+ ## Report Format
93
+
94
+ ```markdown
95
+ ## Security Audit Report
96
+
97
+ ### Summary
98
+ - Critical: X findings
99
+ - High: X findings
100
+ - Medium: X findings
101
+ - Low: X findings
102
+
103
+ ### Critical Findings
104
+
105
+ #### [CRITICAL-001] Title
106
+ - **Location**: file:line
107
+ - **Pattern**: Code snippet
108
+ - **Risk**: Description of the vulnerability
109
+ - **Remediation**: How to fix
110
+ - **Reference**: OWASP link
111
+
112
+ ### High Findings
113
+ [...]
114
+ ```
115
+
116
+ ## OWASP Reference Links
117
+
118
+ For comprehensive guidance, consult these OWASP cheatsheets directly:
119
+
120
+ - XSS Prevention: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
121
+ - DOM XSS Prevention: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html
122
+ - CSRF Prevention: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
123
+ - CSP: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
124
+ - Input Validation: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
125
+ - HTML5 Security: https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html
126
+ - DOM Clobbering: https://cheatsheetseries.owasp.org/cheatsheets/DOM_Clobbering_Prevention_Cheat_Sheet.html
127
+ - Node.js Security: https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html
128
+ - NPM Security: https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html
129
+ - AJAX Security: https://cheatsheetseries.owasp.org/cheatsheets/AJAX_Security_Cheat_Sheet.html
130
+ - File Upload: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
131
+ - Error Handling: https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html
132
+ - JWT Security: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
133
+ - User Privacy: https://cheatsheetseries.owasp.org/cheatsheets/User_Privacy_Protection_Cheat_Sheet.html
134
+ - gRPC Security: https://cheatsheetseries.owasp.org/cheatsheets/gRPC_Security_Cheat_Sheet.html
@@ -0,0 +1,191 @@
1
+ # Content Security Policy Reference
2
+
3
+ ## Strict CSP Configuration
4
+
5
+ ### Nonce-Based (Recommended)
6
+
7
+ ```http
8
+ Content-Security-Policy:
9
+ script-src 'nonce-{random}' 'strict-dynamic';
10
+ object-src 'none';
11
+ base-uri 'none';
12
+ ```
13
+
14
+ Implementation:
15
+
16
+ ```javascript
17
+ // Generate unique nonce per request
18
+ const crypto = require('crypto');
19
+ const nonce = crypto.randomBytes(16).toString('base64');
20
+
21
+ // Set header
22
+ res.setHeader('Content-Security-Policy',
23
+ `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`
24
+ );
25
+
26
+ // Include nonce in script tags
27
+ res.send(`<script nonce="${nonce}">/* safe code */</script>`);
28
+ ```
29
+
30
+ ### Hash-Based (For Static Scripts)
31
+
32
+ ```http
33
+ Content-Security-Policy:
34
+ script-src 'sha256-{hash}' 'strict-dynamic';
35
+ object-src 'none';
36
+ base-uri 'none';
37
+ ```
38
+
39
+ Generate hash:
40
+
41
+ ```bash
42
+ echo -n 'console.log("hello");' | openssl sha256 -binary | openssl base64
43
+ ```
44
+
45
+ ## Essential Directives
46
+
47
+ | Directive | Purpose | Recommended Value |
48
+ |-----------|---------|-------------------|
49
+ | `default-src` | Fallback for other directives | `'self'` |
50
+ | `script-src` | JavaScript sources | `'nonce-{random}' 'strict-dynamic'` |
51
+ | `style-src` | CSS sources | `'self' 'unsafe-inline'` (or nonces) |
52
+ | `img-src` | Image sources | `'self' data: https:` |
53
+ | `font-src` | Font sources | `'self'` |
54
+ | `connect-src` | AJAX/WebSocket/Fetch | `'self' https://api.example.com` |
55
+ | `frame-src` | iframe sources | `'none'` or specific origins |
56
+ | `object-src` | Plugin content | `'none'` |
57
+ | `base-uri` | Base URL restrictions | `'none'` |
58
+ | `form-action` | Form submission targets | `'self'` |
59
+ | `frame-ancestors` | Who can embed page | `'self'` or `'none'` |
60
+
61
+ ## Framework Integration
62
+
63
+ ### Express.js
64
+
65
+ ```javascript
66
+ const helmet = require('helmet');
67
+
68
+ // Nonce middleware
69
+ app.use((req, res, next) => {
70
+ res.locals.nonce = crypto.randomBytes(16).toString('base64');
71
+ next();
72
+ });
73
+
74
+ app.use(helmet.contentSecurityPolicy({
75
+ directives: {
76
+ defaultSrc: ["'self'"],
77
+ scriptSrc: [(req, res) => `'nonce-${res.locals.nonce}'`, "'strict-dynamic'"],
78
+ styleSrc: ["'self'", "'unsafe-inline'"],
79
+ imgSrc: ["'self'", "data:", "https:"],
80
+ objectSrc: ["'none'"],
81
+ baseUri: ["'none'"],
82
+ formAction: ["'self'"],
83
+ frameAncestors: ["'self'"]
84
+ }
85
+ }));
86
+ ```
87
+
88
+ ### Astro
89
+
90
+ ```javascript
91
+ // astro.config.mjs
92
+ export default defineConfig({
93
+ vite: {
94
+ plugins: [{
95
+ name: 'csp-plugin',
96
+ configureServer(server) {
97
+ server.middlewares.use((req, res, next) => {
98
+ res.setHeader('Content-Security-Policy',
99
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
100
+ );
101
+ next();
102
+ });
103
+ }
104
+ }]
105
+ }
106
+ });
107
+ ```
108
+
109
+ ### Meta Tag (Fallback)
110
+
111
+ ```html
112
+ <meta http-equiv="Content-Security-Policy"
113
+ content="default-src 'self'; script-src 'self' 'nonce-abc123';">
114
+ ```
115
+
116
+ **Note**: Meta tag CSP cannot set `frame-ancestors`, `report-uri`, or `sandbox`.
117
+
118
+ ## Report-Only Mode
119
+
120
+ Test policies without enforcement:
121
+
122
+ ```http
123
+ Content-Security-Policy-Report-Only:
124
+ default-src 'self';
125
+ script-src 'self';
126
+ report-uri /csp-report;
127
+ ```
128
+
129
+ Report endpoint:
130
+
131
+ ```javascript
132
+ app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
133
+ console.log('CSP Violation:', req.body);
134
+ res.status(204).end();
135
+ });
136
+ ```
137
+
138
+ ## Common Violations and Fixes
139
+
140
+ ### Inline Scripts
141
+
142
+ ```html
143
+ <!-- Violation: inline script -->
144
+ <script>alert('hello');</script>
145
+
146
+ <!-- Fix: add nonce -->
147
+ <script nonce="abc123">alert('hello');</script>
148
+
149
+ <!-- Or: move to external file -->
150
+ <script src="/js/app.js"></script>
151
+ ```
152
+
153
+ ### Inline Event Handlers
154
+
155
+ ```html
156
+ <!-- Violation: inline event handler -->
157
+ <button onclick="handleClick()">Click</button>
158
+
159
+ <!-- Fix: use addEventListener -->
160
+ <button id="myBtn">Click</button>
161
+ <script nonce="abc123">
162
+ document.getElementById('myBtn').addEventListener('click', handleClick);
163
+ </script>
164
+ ```
165
+
166
+ ### Inline Styles
167
+
168
+ ```html
169
+ <!-- Violation: style attribute -->
170
+ <div style="color: red;">Text</div>
171
+
172
+ <!-- Fix: use classes -->
173
+ <div class="text-red">Text</div>
174
+
175
+ <!-- Or: add nonce to style tags -->
176
+ <style nonce="abc123">.text-red { color: red; }</style>
177
+ ```
178
+
179
+ ## Security Headers Companion
180
+
181
+ Deploy CSP with these additional headers:
182
+
183
+ ```http
184
+ X-Content-Type-Options: nosniff
185
+ X-Frame-Options: DENY
186
+ X-XSS-Protection: 0
187
+ Referrer-Policy: strict-origin-when-cross-origin
188
+ Permissions-Policy: geolocation=(), microphone=(), camera=()
189
+ ```
190
+
191
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
@@ -0,0 +1,327 @@
1
+ # CSRF Protection Reference
2
+
3
+ ## Token-Based Protection
4
+
5
+ ### Synchronizer Token Pattern
6
+
7
+ Generate unique per-session tokens and validate on state-changing requests:
8
+
9
+ ```javascript
10
+ // Server-side token generation (Node.js)
11
+ const crypto = require('crypto');
12
+
13
+ function generateCSRFToken(session) {
14
+ const token = crypto.randomBytes(32).toString('hex');
15
+ session.csrfToken = token;
16
+ return token;
17
+ }
18
+
19
+ // Middleware validation
20
+ function validateCSRF(req, res, next) {
21
+ const token = req.headers['x-csrf-token'] || req.body._csrf;
22
+ const sessionToken = req.session.csrfToken;
23
+
24
+ if (typeof token !== 'string' || typeof sessionToken !== 'string') {
25
+ return res.status(403).json({ error: 'Invalid CSRF token' });
26
+ }
27
+
28
+ const tokenBuffer = Buffer.from(token);
29
+ const sessionTokenBuffer = Buffer.from(sessionToken);
30
+
31
+ if (
32
+ tokenBuffer.length !== sessionTokenBuffer.length ||
33
+ !crypto.timingSafeEqual(tokenBuffer, sessionTokenBuffer)
34
+ ) {
35
+ return res.status(403).json({ error: 'Invalid CSRF token' });
36
+ }
37
+ next();
38
+ }
39
+ ```
40
+
41
+ ### Double Submit Cookie Pattern
42
+
43
+ ```javascript
44
+ // Set CSRF cookie
45
+ res.cookie('csrf_token', token, {
46
+ httpOnly: false, // Must be readable by JavaScript
47
+ secure: true,
48
+ sameSite: 'Strict'
49
+ });
50
+
51
+ // Client sends token in header
52
+ fetch('/api/action', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'X-CSRF-Token': getCookie('csrf_token')
56
+ }
57
+ });
58
+
59
+ // Server validates cookie matches header
60
+ function validateDoubleSubmit(req) {
61
+ const cookieToken = req.cookies.csrf_token;
62
+ const headerToken = req.headers['x-csrf-token'];
63
+ return cookieToken && cookieToken === headerToken;
64
+ }
65
+ ```
66
+
67
+ ## SameSite Cookie Attribute
68
+
69
+ ```javascript
70
+ // Strict - never sent cross-site
71
+ res.cookie('session', value, { sameSite: 'Strict', secure: true, httpOnly: true });
72
+
73
+ // Lax - sent for top-level GET navigations (default in modern browsers)
74
+ res.cookie('session', value, { sameSite: 'Lax', secure: true, httpOnly: true });
75
+
76
+ // None - requires Secure flag, sent cross-site
77
+ res.cookie('session', value, { sameSite: 'None', secure: true, httpOnly: true });
78
+ ```
79
+
80
+ **Recommendation**: Use `SameSite=Strict` for session cookies when possible, `Lax` as minimum.
81
+
82
+ ## Fetch Metadata Headers
83
+
84
+ Validate request origin using Sec-Fetch-* headers:
85
+
86
+ ```javascript
87
+ function validateFetchMetadata(req, res, next) {
88
+ const site = req.headers['sec-fetch-site'];
89
+ const mode = req.headers['sec-fetch-mode'];
90
+ const dest = req.headers['sec-fetch-dest'];
91
+ const method = req.method;
92
+
93
+ // Allow same-origin requests
94
+ if (site === 'same-origin') return next();
95
+
96
+ // Allow user-initiated browser navigations
97
+ if (site === 'none') return next();
98
+
99
+ // Allow same-site top-level navigations and safe methods
100
+ if (
101
+ site === 'same-site' &&
102
+ (mode === 'navigate' || ['GET', 'HEAD', 'OPTIONS'].includes(method))
103
+ ) {
104
+ return next();
105
+ }
106
+
107
+ return res.status(403).json({ error: 'Fetch metadata validation failed' });
108
+ }
109
+ ```
110
+
111
+ ## Framework Integration
112
+
113
+ ### Express.js with Signed Double-Submit Tokens
114
+
115
+ ```javascript
116
+ const crypto = require('crypto');
117
+
118
+ const CSRF_COOKIE_NAME = 'csrf_token';
119
+ const CSRF_SECRET = Buffer.from(process.env.CSRF_SECRET, 'hex');
120
+
121
+ function signToken(sessionId, nonce) {
122
+ return crypto
123
+ .createHmac('sha256', CSRF_SECRET)
124
+ .update(`${sessionId}:${nonce}`)
125
+ .digest('base64url');
126
+ }
127
+
128
+ function constantTimeEqual(value, expected) {
129
+ if (typeof value !== 'string' || typeof expected !== 'string') return false;
130
+
131
+ const valueBuffer = Buffer.from(value);
132
+ const expectedBuffer = Buffer.from(expected);
133
+
134
+ return (
135
+ valueBuffer.length === expectedBuffer.length &&
136
+ crypto.timingSafeEqual(valueBuffer, expectedBuffer)
137
+ );
138
+ }
139
+
140
+ function generateToken(req) {
141
+ const nonce = crypto.randomBytes(32).toString('base64url');
142
+ const signature = signToken(req.session.id, nonce);
143
+ return `${nonce}.${signature}`;
144
+ }
145
+
146
+ function sendToken(req, res, next) {
147
+ const token = generateToken(req);
148
+
149
+ res.cookie(CSRF_COOKIE_NAME, token, {
150
+ httpOnly: true,
151
+ secure: true,
152
+ sameSite: 'Strict'
153
+ });
154
+
155
+ res.locals.csrfToken = token;
156
+ next();
157
+ }
158
+
159
+ function verifyToken(req, res, next) {
160
+ const token = req.headers['x-csrf-token'] || req.body._csrf;
161
+ const cookieToken = req.cookies[CSRF_COOKIE_NAME];
162
+
163
+ if (!constantTimeEqual(token, cookieToken)) {
164
+ return res.status(403).json({ error: 'Invalid CSRF token' });
165
+ }
166
+
167
+ const [nonce, signature, extra] = cookieToken.split('.');
168
+ if (extra || !nonce || !signature) {
169
+ return res.status(403).json({ error: 'Invalid CSRF token' });
170
+ }
171
+
172
+ if (!constantTimeEqual(signature, signToken(req.session.id, nonce))) {
173
+ return res.status(403).json({ error: 'Invalid CSRF token' });
174
+ }
175
+
176
+ next();
177
+ }
178
+
179
+ app.get('/form', sendToken, (req, res) => {
180
+ res.render('form', { csrfToken: res.locals.csrfToken });
181
+ });
182
+
183
+ app.post('/submit', verifyToken, (req, res) => {
184
+ res.json({ ok: true });
185
+ });
186
+ ```
187
+
188
+ Example tests for token issuance and verification:
189
+
190
+ ```javascript
191
+ const assert = require('node:assert/strict');
192
+ const test = require('node:test');
193
+
194
+ test('sendToken issues a cookie and exposes a form token', () => {
195
+ const req = { session: { id: 'session-123' } };
196
+ const res = {
197
+ locals: {},
198
+ cookie(name, value, options) {
199
+ this.cookieArgs = { name, value, options };
200
+ }
201
+ };
202
+
203
+ sendToken(req, res, () => {});
204
+
205
+ assert.equal(res.cookieArgs.name, CSRF_COOKIE_NAME);
206
+ assert.equal(res.locals.csrfToken, res.cookieArgs.value);
207
+ assert.equal(res.cookieArgs.options.httpOnly, true);
208
+ assert.equal(res.cookieArgs.options.sameSite, 'Strict');
209
+ });
210
+
211
+ test('verifyToken accepts a matching signed token', () => {
212
+ const req = { session: { id: 'session-123' } };
213
+ const token = generateToken(req);
214
+ let called = false;
215
+
216
+ verifyToken(
217
+ {
218
+ ...req,
219
+ headers: { 'x-csrf-token': token },
220
+ body: {},
221
+ cookies: { [CSRF_COOKIE_NAME]: token }
222
+ },
223
+ {},
224
+ () => {
225
+ called = true;
226
+ }
227
+ );
228
+
229
+ assert.equal(called, true);
230
+ });
231
+
232
+ test('verifyToken rejects mismatched or tampered tokens', () => {
233
+ const req = {
234
+ session: { id: 'session-123' },
235
+ headers: { 'x-csrf-token': 'tampered.token' },
236
+ body: {},
237
+ cookies: { [CSRF_COOKIE_NAME]: generateToken({ session: { id: 'session-123' } }) }
238
+ };
239
+ const res = {
240
+ status(code) {
241
+ this.statusCode = code;
242
+ return this;
243
+ },
244
+ json(body) {
245
+ this.body = body;
246
+ }
247
+ };
248
+
249
+ verifyToken(req, res, () => {});
250
+
251
+ assert.equal(res.statusCode, 403);
252
+ assert.deepEqual(res.body, { error: 'Invalid CSRF token' });
253
+ });
254
+ ```
255
+
256
+ ### React Forms
257
+
258
+ ```jsx
259
+ function Form({ csrfToken }) {
260
+ const handleSubmit = async (e) => {
261
+ e.preventDefault();
262
+ await fetch('/api/submit', {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json',
266
+ 'X-CSRF-Token': csrfToken
267
+ },
268
+ body: JSON.stringify(formData)
269
+ });
270
+ };
271
+
272
+ return (
273
+ <form onSubmit={handleSubmit}>
274
+ <input type="hidden" name="_csrf" value={csrfToken} />
275
+ {/* form fields */}
276
+ </form>
277
+ );
278
+ }
279
+ ```
280
+
281
+ ### Twig Forms
282
+
283
+ ```twig
284
+ <form method="post" action="/submit">
285
+ <input type="hidden" name="_csrf_token" value="{{ csrf_token('form_name') }}">
286
+ <!-- form fields -->
287
+ </form>
288
+ ```
289
+
290
+ ## Client-Side CSRF (AJAX)
291
+
292
+ Protect against CSRF in single-page applications:
293
+
294
+ ```javascript
295
+ // Set up axios defaults
296
+ import axios from 'axios';
297
+
298
+ axios.defaults.xsrfCookieName = 'csrf_token';
299
+ axios.defaults.xsrfHeaderName = 'X-CSRF-Token';
300
+ axios.defaults.withCredentials = true;
301
+
302
+ // Or with fetch
303
+ async function secureFetch(url, options = {}) {
304
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
305
+
306
+ return fetch(url, {
307
+ ...options,
308
+ credentials: 'same-origin',
309
+ headers: {
310
+ ...options.headers,
311
+ 'X-CSRF-Token': csrfToken
312
+ }
313
+ });
314
+ }
315
+ ```
316
+
317
+ ## Verification Checklist
318
+
319
+ - [ ] All state-changing endpoints require CSRF tokens
320
+ - [ ] Tokens are cryptographically random (≥128 bits)
321
+ - [ ] Tokens are tied to user session
322
+ - [ ] Tokens validated server-side before processing
323
+ - [ ] SameSite cookie attribute set appropriately
324
+ - [ ] Fetch Metadata headers validated for sensitive endpoints
325
+ - [ ] GET requests are idempotent (no state changes)
326
+
327
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html