@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.
- package/README.md +29 -7
- package/dist/index.mjs +90 -6
- package/dist/index.mjs.map +1 -1
- package/hooks/auto-approve-safe-commands/hook.mjs +134 -0
- package/hooks/auto-approve-safe-commands/hook.mts +188 -0
- package/hooks/auto-approve-safe-commands/settings-fragment.json +17 -0
- package/hooks/block-dangerous-commands/hook.mjs +3 -3
- package/hooks/block-dangerous-commands/hook.mts +23 -10
- package/package.json +8 -10
- 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,307 @@
|
|
|
1
|
+
# Framework-Specific Security Patterns
|
|
2
|
+
|
|
3
|
+
## React Security
|
|
4
|
+
|
|
5
|
+
### XSS Prevention
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
// DEFAULT SAFE - React escapes by default
|
|
9
|
+
<div>{userInput}</div>
|
|
10
|
+
|
|
11
|
+
// DANGEROUS - bypasses escaping
|
|
12
|
+
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
|
13
|
+
|
|
14
|
+
// If HTML is required, sanitize first
|
|
15
|
+
import DOMPurify from 'dompurify';
|
|
16
|
+
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### URL Handling
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
// DANGEROUS - javascript: URLs in href
|
|
23
|
+
<a href={userInput}>Link</a>
|
|
24
|
+
|
|
25
|
+
// SAFE - validate URL protocol
|
|
26
|
+
function SafeLink({ href, children }) {
|
|
27
|
+
const safeHref = useMemo(() => {
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(href, window.location.origin);
|
|
30
|
+
if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
|
|
31
|
+
return href;
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
return '#';
|
|
35
|
+
}, [href]);
|
|
36
|
+
|
|
37
|
+
return <a href={safeHref}>{children}</a>;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### State and Props
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
// DANGEROUS - spreading user-controlled props
|
|
45
|
+
<Component {...userControlledObject} />
|
|
46
|
+
|
|
47
|
+
// SAFE - explicitly pass allowed props
|
|
48
|
+
<Component
|
|
49
|
+
title={userControlledObject.title}
|
|
50
|
+
description={userControlledObject.description}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Server-Side Rendering (SSR)
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
// DANGEROUS - injecting user data into SSR without escaping
|
|
58
|
+
<script>
|
|
59
|
+
window.__INITIAL_STATE__ = {JSON.stringify(userControlledData)}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
// SAFE - serialize with escaping
|
|
63
|
+
import serialize from 'serialize-javascript';
|
|
64
|
+
<script
|
|
65
|
+
dangerouslySetInnerHTML={{
|
|
66
|
+
__html: `window.__INITIAL_STATE__ = ${serialize(data, { isJSON: true })}`
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Astro Security
|
|
72
|
+
|
|
73
|
+
### Content Escaping
|
|
74
|
+
|
|
75
|
+
```astro
|
|
76
|
+
---
|
|
77
|
+
const userInput = Astro.props.userInput;
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
<!-- SAFE - auto-escaped -->
|
|
81
|
+
<div>{userInput}</div>
|
|
82
|
+
|
|
83
|
+
<!-- DANGEROUS - bypasses escaping -->
|
|
84
|
+
<div set:html={userInput} />
|
|
85
|
+
|
|
86
|
+
<!-- If HTML required, sanitize -->
|
|
87
|
+
---
|
|
88
|
+
import DOMPurify from 'dompurify';
|
|
89
|
+
const sanitized = DOMPurify.sanitize(userInput);
|
|
90
|
+
---
|
|
91
|
+
<div set:html={sanitized} />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Dynamic Imports
|
|
95
|
+
|
|
96
|
+
```astro
|
|
97
|
+
---
|
|
98
|
+
// DANGEROUS - user-controlled import path
|
|
99
|
+
const component = await import(userInput);
|
|
100
|
+
|
|
101
|
+
// SAFE - allowlist approach
|
|
102
|
+
const allowedComponents = {
|
|
103
|
+
'card': () => import('./Card.astro'),
|
|
104
|
+
'button': () => import('./Button.astro')
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const loadComponent = allowedComponents[userInput];
|
|
108
|
+
if (!loadComponent) throw new Error('Invalid component');
|
|
109
|
+
const Component = await loadComponent();
|
|
110
|
+
---
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### API Endpoints
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
// src/pages/api/data.js
|
|
117
|
+
export async function POST({ request }) {
|
|
118
|
+
// Validate Content-Type
|
|
119
|
+
const contentType = request.headers.get('content-type');
|
|
120
|
+
if (!contentType?.includes('application/json')) {
|
|
121
|
+
return new Response('Invalid content type', { status: 415 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate and sanitize input
|
|
125
|
+
const body = await request.json();
|
|
126
|
+
if (!validateInput(body)) {
|
|
127
|
+
return new Response('Invalid input', { status: 400 });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Process request
|
|
131
|
+
return new Response(JSON.stringify(result), {
|
|
132
|
+
headers: { 'Content-Type': 'application/json' }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Twig Security
|
|
138
|
+
|
|
139
|
+
### Output Escaping
|
|
140
|
+
|
|
141
|
+
```twig
|
|
142
|
+
{# SAFE - auto-escaped for HTML context #}
|
|
143
|
+
{{ userInput }}
|
|
144
|
+
|
|
145
|
+
{# DANGEROUS - raw bypasses escaping #}
|
|
146
|
+
{{ userInput|raw }}
|
|
147
|
+
|
|
148
|
+
{# DANGEROUS - autoescape disabled #}
|
|
149
|
+
{% autoescape false %}
|
|
150
|
+
{{ userInput }}
|
|
151
|
+
{% endautoescape %}
|
|
152
|
+
|
|
153
|
+
{# Context-specific escaping #}
|
|
154
|
+
{{ userInput|e('html') }}
|
|
155
|
+
{{ userInput|e('js') }}
|
|
156
|
+
{{ userInput|e('css') }}
|
|
157
|
+
{{ userInput|e('url') }}
|
|
158
|
+
{{ userInput|e('html_attr') }}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Template Inclusion
|
|
162
|
+
|
|
163
|
+
```twig
|
|
164
|
+
{# DANGEROUS - user-controlled template path #}
|
|
165
|
+
{% include userInput %}
|
|
166
|
+
|
|
167
|
+
{# SAFE - use allowlist #}
|
|
168
|
+
{% if templateName in ['header', 'footer', 'sidebar'] %}
|
|
169
|
+
{% include templateName ~ '.html.twig' %}
|
|
170
|
+
{% endif %}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Sandbox Mode (Symfony)
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
# config/packages/twig.yaml
|
|
177
|
+
twig:
|
|
178
|
+
sandbox:
|
|
179
|
+
policy:
|
|
180
|
+
tags: ['if', 'for', 'set']
|
|
181
|
+
filters: ['escape', 'upper', 'lower']
|
|
182
|
+
methods:
|
|
183
|
+
Symfony\Component\Routing\Generator\UrlGeneratorInterface: ['generate']
|
|
184
|
+
properties: []
|
|
185
|
+
functions: ['path', 'url']
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### CSRF in Forms
|
|
189
|
+
|
|
190
|
+
```twig
|
|
191
|
+
{# Symfony CSRF protection #}
|
|
192
|
+
<form method="post">
|
|
193
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token('form_name') }}">
|
|
194
|
+
{# form fields #}
|
|
195
|
+
</form>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Bun Security
|
|
199
|
+
|
|
200
|
+
### Request Handling
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
// Bun HTTP server
|
|
204
|
+
Bun.serve({
|
|
205
|
+
port: 3000,
|
|
206
|
+
fetch(req) {
|
|
207
|
+
const url = new URL(req.url);
|
|
208
|
+
|
|
209
|
+
// Validate origin for CORS
|
|
210
|
+
const origin = req.headers.get('origin');
|
|
211
|
+
if (origin && !isAllowedOrigin(origin)) {
|
|
212
|
+
return new Response('Forbidden', { status: 403 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Rate limiting
|
|
216
|
+
if (isRateLimited(req)) {
|
|
217
|
+
return new Response('Too Many Requests', { status: 429 });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return handleRequest(req);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### File Handling
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
// Validate file paths
|
|
229
|
+
function safeReadFile(userPath) {
|
|
230
|
+
const baseDir = '/app/public';
|
|
231
|
+
const resolved = Bun.resolveSync(userPath, baseDir);
|
|
232
|
+
|
|
233
|
+
if (!resolved.startsWith(baseDir)) {
|
|
234
|
+
throw new Error('Path traversal detected');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Bun.file(resolved).text();
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## HTML5 APIs Security
|
|
242
|
+
|
|
243
|
+
### Web Storage
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
// NEVER store sensitive data in localStorage
|
|
247
|
+
localStorage.setItem('token', jwt); // DANGEROUS
|
|
248
|
+
|
|
249
|
+
// Use httpOnly cookies for tokens instead
|
|
250
|
+
// Or store in memory with short expiration
|
|
251
|
+
|
|
252
|
+
// If localStorage is necessary, encrypt
|
|
253
|
+
import { encrypt, decrypt } from './crypto';
|
|
254
|
+
localStorage.setItem('data', encrypt(sensitiveData, key));
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### postMessage
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
// Always validate origin and data
|
|
261
|
+
window.addEventListener('message', (event) => {
|
|
262
|
+
// Validate origin
|
|
263
|
+
const allowedOrigins = ['https://trusted.com'];
|
|
264
|
+
if (!allowedOrigins.includes(event.origin)) return;
|
|
265
|
+
|
|
266
|
+
// Validate data structure
|
|
267
|
+
if (typeof event.data !== 'object') return;
|
|
268
|
+
if (!['action1', 'action2'].includes(event.data.type)) return;
|
|
269
|
+
|
|
270
|
+
handleMessage(event.data);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Always specify target origin when sending
|
|
274
|
+
iframe.contentWindow.postMessage(data, 'https://specific-origin.com');
|
|
275
|
+
// NEVER use '*' for sensitive data
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### WebSockets
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
// Validate WebSocket origin
|
|
282
|
+
const wss = new WebSocket.Server({
|
|
283
|
+
server,
|
|
284
|
+
verifyClient: ({ origin, req }, callback) => {
|
|
285
|
+
const allowed = ['https://myapp.com'];
|
|
286
|
+
callback(allowed.includes(origin));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Validate messages
|
|
291
|
+
wss.on('connection', (ws) => {
|
|
292
|
+
ws.on('message', (data) => {
|
|
293
|
+
try {
|
|
294
|
+
const msg = JSON.parse(data);
|
|
295
|
+
if (!isValidMessage(msg)) {
|
|
296
|
+
ws.close(1008, 'Invalid message');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
handleMessage(msg);
|
|
300
|
+
} catch {
|
|
301
|
+
ws.close(1008, 'Invalid JSON');
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Input Validation Reference
|
|
2
|
+
|
|
3
|
+
## Validation Strategy
|
|
4
|
+
|
|
5
|
+
**Always validate on the server.** Client-side validation improves UX but provides no security.
|
|
6
|
+
|
|
7
|
+
### Allowlist vs Denylist
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
// PREFERRED: Allowlist (accept known good)
|
|
11
|
+
function validateUsername(input) {
|
|
12
|
+
const allowedPattern = /^[a-zA-Z0-9_]{3,20}$/;
|
|
13
|
+
return allowedPattern.test(input);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// AVOID: Denylist (block known bad)
|
|
17
|
+
function validateInput(input) {
|
|
18
|
+
const blocked = ['<script>', 'javascript:', 'onerror'];
|
|
19
|
+
return !blocked.some(bad => input.includes(bad)); // Easily bypassed
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Common Validation Patterns
|
|
24
|
+
|
|
25
|
+
### Email
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// Basic validation (server should still verify)
|
|
29
|
+
function validateEmail(email) {
|
|
30
|
+
// Simple pattern - not comprehensive but catches most issues
|
|
31
|
+
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
32
|
+
return pattern.test(email) && email.length <= 254;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use built-in browser validation
|
|
36
|
+
const input = document.createElement('input');
|
|
37
|
+
input.type = 'email';
|
|
38
|
+
input.value = email;
|
|
39
|
+
return input.checkValidity();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### URL
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
function validateUrl(input) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(input);
|
|
48
|
+
return ['http:', 'https:'].includes(url.protocol);
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// For user-facing URLs, also check for malicious patterns
|
|
55
|
+
function validateSafeUrl(input) {
|
|
56
|
+
const url = validateUrl(input);
|
|
57
|
+
if (!url) return false;
|
|
58
|
+
|
|
59
|
+
// Block data: and javascript: schemes
|
|
60
|
+
const dangerous = ['javascript:', 'data:', 'vbscript:'];
|
|
61
|
+
return !dangerous.some(scheme => input.toLowerCase().startsWith(scheme));
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Numbers
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
function validateInteger(input, min, max) {
|
|
69
|
+
const num = parseInt(input, 10);
|
|
70
|
+
if (isNaN(num)) return false;
|
|
71
|
+
if (num.toString() !== input.toString()) return false; // Reject "123abc"
|
|
72
|
+
return num >= min && num <= max;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateDecimal(input, min, max, decimals) {
|
|
76
|
+
const num = parseFloat(input);
|
|
77
|
+
if (isNaN(num)) return false;
|
|
78
|
+
if (num < min || num > max) return false;
|
|
79
|
+
|
|
80
|
+
const parts = input.split('.');
|
|
81
|
+
if (parts.length > 2) return false;
|
|
82
|
+
if (parts[1] && parts[1].length > decimals) return false;
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Date
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
function validateDate(input) {
|
|
92
|
+
const date = new Date(input);
|
|
93
|
+
return date instanceof Date && !isNaN(date);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateDateRange(input, minDate, maxDate) {
|
|
97
|
+
const date = new Date(input);
|
|
98
|
+
if (isNaN(date)) return false;
|
|
99
|
+
return date >= minDate && date <= maxDate;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Phone Numbers
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
// International format
|
|
107
|
+
function validatePhone(input) {
|
|
108
|
+
// E.164 format: +[country][number], max 15 digits
|
|
109
|
+
const pattern = /^\+[1-9]\d{1,14}$/;
|
|
110
|
+
return pattern.test(input.replace(/[\s\-()]/g, ''));
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Sanitization Functions
|
|
115
|
+
|
|
116
|
+
### HTML Entities
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
function escapeHtml(input) {
|
|
120
|
+
const map = {
|
|
121
|
+
'&': '&',
|
|
122
|
+
'<': '<',
|
|
123
|
+
'>': '>',
|
|
124
|
+
'"': '"',
|
|
125
|
+
"'": ''',
|
|
126
|
+
'/': '/'
|
|
127
|
+
};
|
|
128
|
+
return String(input).replace(/[&<>"'/]/g, char => map[char]);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### SQL (Use Parameterized Queries Instead)
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// WRONG - never build SQL strings
|
|
136
|
+
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
|
|
137
|
+
|
|
138
|
+
// RIGHT - use parameterized queries
|
|
139
|
+
const query = 'SELECT * FROM users WHERE name = ?';
|
|
140
|
+
db.query(query, [userInput]);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Path Traversal Prevention
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
const path = require('path');
|
|
147
|
+
|
|
148
|
+
function validateFilePath(userPath, baseDir) {
|
|
149
|
+
const baseCanonical = path.resolve(baseDir);
|
|
150
|
+
const resolved = path.resolve(baseDir, userPath);
|
|
151
|
+
const relativePath = path.relative(baseCanonical, resolved);
|
|
152
|
+
|
|
153
|
+
// Ensure resolved path stays inside the base directory
|
|
154
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
155
|
+
throw new Error('Path traversal detected');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resolved;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Framework Validation
|
|
163
|
+
|
|
164
|
+
### Node.js with Joi
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
const Joi = require('joi');
|
|
168
|
+
|
|
169
|
+
const userSchema = Joi.object({
|
|
170
|
+
username: Joi.string().alphanum().min(3).max(30).required(),
|
|
171
|
+
email: Joi.string().email().required(),
|
|
172
|
+
age: Joi.number().integer().min(0).max(150),
|
|
173
|
+
website: Joi.string().uri({ scheme: ['http', 'https'] })
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function validateUser(data) {
|
|
177
|
+
const { error, value } = userSchema.validate(data);
|
|
178
|
+
if (error) throw new Error(error.details[0].message);
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Express Validator
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
const { body, validationResult } = require('express-validator');
|
|
187
|
+
|
|
188
|
+
app.post('/user',
|
|
189
|
+
body('email').isEmail().normalizeEmail(),
|
|
190
|
+
body('password').isLength({ min: 8 }),
|
|
191
|
+
body('age').isInt({ min: 0, max: 150 }),
|
|
192
|
+
(req, res) => {
|
|
193
|
+
const errors = validationResult(req);
|
|
194
|
+
if (!errors.isEmpty()) {
|
|
195
|
+
return res.status(400).json({ errors: errors.array() });
|
|
196
|
+
}
|
|
197
|
+
// Process valid input
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Zod (TypeScript)
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { z } from 'zod';
|
|
206
|
+
|
|
207
|
+
const UserSchema = z.object({
|
|
208
|
+
username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/),
|
|
209
|
+
email: z.string().email(),
|
|
210
|
+
age: z.number().int().min(0).max(150).optional(),
|
|
211
|
+
website: z.string().url().optional()
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
type User = z.infer<typeof UserSchema>;
|
|
215
|
+
|
|
216
|
+
function validateUser(data: unknown): User {
|
|
217
|
+
return UserSchema.parse(data);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Validation Checklist
|
|
222
|
+
|
|
223
|
+
- [ ] Validate all input on the server
|
|
224
|
+
- [ ] Use allowlist validation when possible
|
|
225
|
+
- [ ] Validate data type, length, format, and range
|
|
226
|
+
- [ ] Reject unexpected input rather than sanitizing
|
|
227
|
+
- [ ] Use parameterized queries for database operations
|
|
228
|
+
- [ ] Validate file uploads (type, size, content)
|
|
229
|
+
- [ ] Canonicalize paths before validation
|
|
230
|
+
- [ ] Log validation failures for monitoring
|
|
231
|
+
|
|
232
|
+
OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
|