@schalkneethling/calavera-skill-frontend-security 0.2.0-next.2

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,328 @@
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
+ // SAFER - parse and enforce allowed protocols and destinations
26
+ function SafeLink({ href, children }) {
27
+ const safeHref = useMemo(() => {
28
+ try {
29
+ const url = new URL(href, window.location.origin);
30
+ if (!["https:", "mailto:"].includes(url.protocol)) return "#";
31
+ if (
32
+ url.protocol === "https:" &&
33
+ !["https://example.com", "https://docs.example.com"].includes(url.origin)
34
+ ) {
35
+ return "#";
36
+ }
37
+ return url.href;
38
+ } catch {}
39
+ return "#";
40
+ }, [href]);
41
+
42
+ return (
43
+ <a href={safeHref} target="_blank" rel="noopener noreferrer">
44
+ {children}
45
+ </a>
46
+ );
47
+ }
48
+ ```
49
+
50
+ ### State and Props
51
+
52
+ ```jsx
53
+ // DANGEROUS - spreading user-controlled props
54
+ <Component {...userControlledObject} />
55
+
56
+ // SAFE - explicitly pass allowed props
57
+ <Component
58
+ title={userControlledObject.title}
59
+ description={userControlledObject.description}
60
+ />
61
+ ```
62
+
63
+ ### Server-Side Rendering (SSR)
64
+
65
+ ```jsx
66
+ // DANGEROUS - injecting user data into SSR without escaping
67
+ <script>window.__INITIAL_STATE__ = {JSON.stringify(userControlledData)}</script>;
68
+
69
+ // SAFE - serialize with escaping
70
+ import serialize from "serialize-javascript";
71
+ <script
72
+ dangerouslySetInnerHTML={{
73
+ __html: `window.__INITIAL_STATE__ = ${serialize(data, { isJSON: true })}`,
74
+ }}
75
+ />;
76
+ ```
77
+
78
+ ## Astro Security
79
+
80
+ ### Content Escaping
81
+
82
+ ```astro
83
+ ---
84
+ const userInput = Astro.props.userInput;
85
+ ---
86
+
87
+ <!-- SAFE - auto-escaped -->
88
+ <div>{userInput}</div>
89
+
90
+ <!-- DANGEROUS - bypasses escaping -->
91
+ <div set:html={userInput} />
92
+
93
+ <!-- If HTML required, sanitize -->
94
+ ---
95
+ import DOMPurify from 'dompurify';
96
+ const sanitized = DOMPurify.sanitize(userInput);
97
+ ---
98
+ <div set:html={sanitized} />
99
+ ```
100
+
101
+ ### Dynamic Imports
102
+
103
+ ```astro
104
+ ---
105
+ // DANGEROUS - user-controlled import path
106
+ const component = await import(userInput);
107
+
108
+ // SAFE - allowlist approach
109
+ const allowedComponents = {
110
+ 'card': () => import('./Card.astro'),
111
+ 'button': () => import('./Button.astro')
112
+ };
113
+
114
+ if (!Object.hasOwn(allowedComponents, userInput)) {
115
+ throw new Error('Invalid component');
116
+ }
117
+ const loadComponent = allowedComponents[userInput];
118
+ const Component = await loadComponent();
119
+ ---
120
+ ```
121
+
122
+ ### API Endpoints
123
+
124
+ ```javascript
125
+ // src/pages/api/data.js
126
+ export async function POST({ request }) {
127
+ // Validate Content-Type
128
+ const contentType = request.headers.get("content-type");
129
+ if (!contentType?.includes("application/json")) {
130
+ return new Response("Invalid content type", { status: 415 });
131
+ }
132
+
133
+ // Validate and sanitize input
134
+ const body = await request.json();
135
+ if (!validateInput(body)) {
136
+ return new Response("Invalid input", { status: 400 });
137
+ }
138
+
139
+ // Process request
140
+ const responsePayload = { ok: true };
141
+
142
+ return new Response(JSON.stringify(responsePayload), {
143
+ headers: { "Content-Type": "application/json" },
144
+ });
145
+ }
146
+ ```
147
+
148
+ ## Twig Security
149
+
150
+ ### Output Escaping
151
+
152
+ ```twig
153
+ {# SAFE - auto-escaped for HTML context #}
154
+ {{ userInput }}
155
+
156
+ {# DANGEROUS - raw bypasses escaping #}
157
+ {{ userInput|raw }}
158
+
159
+ {# DANGEROUS - autoescape disabled #}
160
+ {% autoescape false %}
161
+ {{ userInput }}
162
+ {% endautoescape %}
163
+
164
+ {# Context-specific escaping #}
165
+ {{ userInput|e('html') }}
166
+ {{ userInput|e('js') }}
167
+ {{ userInput|e('css') }}
168
+ {{ userInput|e('url') }}
169
+ {{ userInput|e('html_attr') }}
170
+ ```
171
+
172
+ ### Template Inclusion
173
+
174
+ ```twig
175
+ {# DANGEROUS - user-controlled template path #}
176
+ {% include userInput %}
177
+
178
+ {# SAFE - use allowlist #}
179
+ {% if templateName in ['header', 'footer', 'sidebar'] %}
180
+ {% include templateName ~ '.html.twig' %}
181
+ {% endif %}
182
+ ```
183
+
184
+ ### Sandbox Mode (Symfony)
185
+
186
+ ```yaml
187
+ # config/packages/twig.yaml
188
+ twig:
189
+ sandbox:
190
+ policy:
191
+ tags: ["if", "for", "set"]
192
+ filters: ["escape", "upper", "lower"]
193
+ methods:
194
+ Symfony\Component\Routing\Generator\UrlGeneratorInterface: ["generate"]
195
+ properties: []
196
+ functions: ["path", "url"]
197
+ ```
198
+
199
+ ### CSRF in Forms
200
+
201
+ ```twig
202
+ {# Symfony CSRF protection #}
203
+ <form method="post">
204
+ <input type="hidden" name="_csrf_token" value="{{ csrf_token('form_name') }}">
205
+ {# form fields #}
206
+ </form>
207
+ ```
208
+
209
+ ## Bun Security
210
+
211
+ ### Request Handling
212
+
213
+ ```javascript
214
+ // Bun HTTP server
215
+ Bun.serve({
216
+ port: 3000,
217
+ fetch(req) {
218
+ const url = new URL(req.url);
219
+
220
+ // Validate origin for CORS
221
+ const origin = req.headers.get("origin");
222
+ if (origin && !isAllowedOrigin(origin)) {
223
+ return new Response("Forbidden", { status: 403 });
224
+ }
225
+
226
+ // Rate limiting
227
+ if (isRateLimited(req)) {
228
+ return new Response("Too Many Requests", { status: 429 });
229
+ }
230
+
231
+ return handleRequest(req);
232
+ },
233
+ });
234
+ ```
235
+
236
+ ### File Handling
237
+
238
+ ```javascript
239
+ const { constants } = require("node:fs");
240
+ const fs = require("node:fs/promises");
241
+ const path = require("node:path");
242
+
243
+ // Keep baseDir trusted and non-writable by the request user.
244
+ async function safeReadFile(userPath) {
245
+ const baseCanonical = await fs.realpath("/app/public");
246
+ const targetCanonical = await fs.realpath(path.resolve(baseCanonical, userPath));
247
+ const relativePath = path.relative(baseCanonical, targetCanonical);
248
+
249
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
250
+ throw new Error("Path traversal detected");
251
+ }
252
+
253
+ // O_NOFOLLOW rejects a final-component symlink swap. Trusted, non-writable
254
+ // parent directories prevent intermediate-component replacement.
255
+ const file = await fs.open(targetCanonical, constants.O_RDONLY | constants.O_NOFOLLOW);
256
+ try {
257
+ return await file.readFile("utf8");
258
+ } finally {
259
+ await file.close();
260
+ }
261
+ }
262
+ ```
263
+
264
+ ## HTML5 APIs Security
265
+
266
+ ### Web Storage
267
+
268
+ ```javascript
269
+ // NEVER store sensitive data in localStorage
270
+ localStorage.setItem("token", jwt); // DANGEROUS
271
+
272
+ // Prefer server-side sessions, httpOnly cookies, or in-memory short-lived tokens.
273
+ // Client-side encryption does not protect sensitive data from XSS when the key
274
+ // is also available to frontend JavaScript.
275
+ ```
276
+
277
+ ### postMessage
278
+
279
+ ```javascript
280
+ // Always validate origin and data
281
+ window.addEventListener("message", (event) => {
282
+ // Validate origin
283
+ const allowedOrigins = ["https://trusted.com"];
284
+ if (!allowedOrigins.includes(event.origin)) return;
285
+
286
+ // Validate data structure
287
+ const data = event.data;
288
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return;
289
+ if (!["action1", "action2"].includes(data.type)) return;
290
+
291
+ handleMessage(data);
292
+ });
293
+
294
+ // Always specify target origin when sending
295
+ iframe.contentWindow.postMessage(data, "https://specific-origin.com");
296
+ // NEVER use '*' for sensitive data
297
+ ```
298
+
299
+ ### WebSockets
300
+
301
+ ```javascript
302
+ // Validate WebSocket origin
303
+ const wss = new WebSocket.Server({
304
+ server,
305
+ verifyClient: ({ origin, req }, callback) => {
306
+ const allowed = ["https://myapp.com"];
307
+ callback(allowed.includes(origin));
308
+ },
309
+ });
310
+
311
+ // Validate messages
312
+ wss.on("connection", (ws) => {
313
+ ws.on("message", (data) => {
314
+ try {
315
+ const msg = JSON.parse(data);
316
+ if (!isValidMessage(msg)) {
317
+ ws.close(1008, "Invalid message");
318
+ return;
319
+ }
320
+ handleMessage(msg);
321
+ } catch {
322
+ ws.close(1008, "Invalid JSON");
323
+ }
324
+ });
325
+ });
326
+ ```
327
+
328
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html
@@ -0,0 +1,360 @@
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
+ Validate input against the exact type, format, and business rule the application
8
+ will use. When parsing is required, validate and then consume the parsed or
9
+ normalized value instead of reusing the original string. Parser confusion is a
10
+ common source of validation bypasses.
11
+
12
+ ### Allowlist vs Denylist
13
+
14
+ ```javascript
15
+ // PREFERRED: Allowlist (accept known good)
16
+ function validateUsername(input) {
17
+ const allowedPattern = /^[a-zA-Z0-9_]{3,20}$/;
18
+ return typeof input === "string" && allowedPattern.test(input);
19
+ }
20
+
21
+ // AVOID: Denylist (block known bad)
22
+ function validateInput(input) {
23
+ const blocked = ["<script>", "javascript:", "onerror"];
24
+ return !blocked.some((bad) => input.includes(bad)); // Easily bypassed
25
+ }
26
+ ```
27
+
28
+ ## Common Validation Patterns
29
+
30
+ ### Email
31
+
32
+ ```javascript
33
+ // Basic validation (server should still verify)
34
+ function validateEmail(email) {
35
+ if (typeof email !== "string") return false;
36
+ // Simple pattern - not comprehensive but catches most issues
37
+ const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
38
+ return pattern.test(email) && email.length <= 254;
39
+ }
40
+
41
+ // Use built-in browser validation
42
+ const input = document.createElement("input");
43
+ input.type = "email";
44
+ input.value = email;
45
+ return input.checkValidity();
46
+ ```
47
+
48
+ ### URL
49
+
50
+ ```javascript
51
+ function matchesPathPrefix(pathname, allowedPrefix) {
52
+ return (
53
+ allowedPrefix === "/" || pathname === allowedPrefix || pathname.startsWith(`${allowedPrefix}/`)
54
+ );
55
+ }
56
+
57
+ function parseAllowedHttpUrl(input, { baseUrl, allowedOrigins, allowedPathPrefixes = ["/"] }) {
58
+ try {
59
+ const allowedOriginSet = new Set(allowedOrigins.map((origin) => new URL(origin).origin));
60
+ const url = new URL(input, baseUrl);
61
+
62
+ if (!["http:", "https:"].includes(url.protocol)) return null;
63
+ if (url.username || url.password) return null;
64
+ if (!allowedOriginSet.has(url.origin)) return null;
65
+ if (!allowedPathPrefixes.some((prefix) => matchesPathPrefix(url.pathname, prefix))) return null;
66
+
67
+ return url;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ const url = parseAllowedHttpUrl(input, {
74
+ baseUrl: "https://example.com",
75
+ allowedOrigins: ["https://example.com", "https://docs.example.com"],
76
+ allowedPathPrefixes: ["/account", "/docs"],
77
+ });
78
+ if (!url) throw new Error("Invalid URL");
79
+
80
+ redirectTo(url.href); // Use the normalized URL returned by the parser.
81
+ ```
82
+
83
+ Do not validate only the scheme and then use the original string for redirects,
84
+ links, fetches, or storage. Validate the full use case: protocol, origin, path,
85
+ credential-bearing URLs, and whether relative URLs are allowed.
86
+
87
+ ### Numbers
88
+
89
+ ```javascript
90
+ function parseInteger(input, min, max) {
91
+ if (typeof input !== "string" || !/^-?(0|[1-9]\d*)$/.test(input)) return null;
92
+ const num = Number(input);
93
+ if (!Number.isSafeInteger(num)) return null;
94
+ if (num < min || num > max) return null;
95
+ return num;
96
+ }
97
+
98
+ function parseDecimal(input, min, max, decimals) {
99
+ if (typeof input !== "string") return null;
100
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 20) return null;
101
+
102
+ const decimalPart = decimals === 0 ? "" : `(?:\\.\\d{1,${decimals}})?`;
103
+ const pattern = new RegExp(`^-?(?:0|[1-9]\\d*)${decimalPart}$`);
104
+ if (!pattern.test(input)) return null;
105
+
106
+ const num = Number(input);
107
+ if (!Number.isFinite(num)) return null;
108
+ if (num < min || num > max) return null;
109
+
110
+ return num;
111
+ }
112
+ ```
113
+
114
+ Use full-string format checks before numeric conversion. `parseInt` and
115
+ `parseFloat` are not validators because they can accept partial strings such as
116
+ `"1abc"`. For money, measurements, or other precision-sensitive values, prefer
117
+ minor units or a decimal library instead of JavaScript floating-point numbers.
118
+
119
+ ### Date
120
+
121
+ ```javascript
122
+ function parseIsoDateOnly(input) {
123
+ if (typeof input !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(input)) return null;
124
+
125
+ const [year, month, day] = input.split("-").map(Number);
126
+ const date = new Date(0);
127
+ date.setUTCHours(0, 0, 0, 0);
128
+ date.setUTCFullYear(year, month - 1, day);
129
+
130
+ if (
131
+ date.getUTCFullYear() !== year ||
132
+ date.getUTCMonth() !== month - 1 ||
133
+ date.getUTCDate() !== day
134
+ ) {
135
+ return null;
136
+ }
137
+
138
+ return input;
139
+ }
140
+
141
+ function validateDateRange(input, minDate, maxDate) {
142
+ const date = parseIsoDateOnly(input);
143
+ const min = parseIsoDateOnly(minDate);
144
+ const max = parseIsoDateOnly(maxDate);
145
+
146
+ if (!date || !min || !max) return false;
147
+ return date >= min && date <= max;
148
+ }
149
+ ```
150
+
151
+ Avoid free-form `Date.parse()` for validation. JavaScript date parsing accepts
152
+ multiple formats, can normalize invalid calendar dates, and may behave
153
+ differently for ambiguous inputs. Prefer explicit formats and round-trip checks.
154
+
155
+ ### Phone Numbers
156
+
157
+ ```javascript
158
+ // International format
159
+ function validatePhone(input) {
160
+ if (typeof input !== "string") return false;
161
+ // E.164 format: +[country][number], max 15 digits
162
+ const pattern = /^\+[1-9]\d{1,14}$/;
163
+ return pattern.test(input.replace(/[\s\-()]/g, ""));
164
+ }
165
+ ```
166
+
167
+ ## Output Encoding and Sanitization
168
+
169
+ Validation decides whether data is acceptable. Output encoding makes accepted
170
+ data safe for a specific rendering context. HTML sanitization removes unsafe
171
+ markup when the product intentionally accepts rich HTML.
172
+
173
+ | Context | Safer pattern |
174
+ | ------------------- | ------------------------------------------------------------------------ |
175
+ | HTML text | Framework escaping, `textContent`, or an HTML text encoder |
176
+ | HTML attribute | Framework attribute binding or a context-aware encoder |
177
+ | URL attribute | Parse and allowlist URL, then assign through safe DOM/framework APIs |
178
+ | JavaScript string | Avoid inline script data; use JSON script data or framework data binding |
179
+ | CSS value | Avoid dynamic CSS or validate against a strict allowlist |
180
+ | Rich HTML fragments | Sanitize with a maintained HTML sanitizer such as DOMPurify |
181
+
182
+ ### HTML Text Encoding
183
+
184
+ ```javascript
185
+ // Only use this for HTML text-node content. Attribute, JavaScript, CSS, and URL
186
+ // contexts need their own encoders or framework-supported safe APIs.
187
+ function escapeHtml(input) {
188
+ const map = {
189
+ "&": "&amp;",
190
+ "<": "&lt;",
191
+ ">": "&gt;",
192
+ '"': "&quot;",
193
+ "'": "&#x27;",
194
+ "/": "&#x2F;",
195
+ };
196
+ return String(input).replace(/[&<>"'/]/g, (char) => map[char]);
197
+ }
198
+ ```
199
+
200
+ Prefer DOM or framework APIs that encode for you:
201
+
202
+ ```javascript
203
+ element.textContent = userInput;
204
+ ```
205
+
206
+ Manual escaping is easy to misuse outside its intended context. Do not treat
207
+ HTML text encoding as URL validation, JavaScript escaping, CSS escaping, SQL
208
+ escaping, or HTML sanitization.
209
+
210
+ ### HTML Sanitization
211
+
212
+ ```javascript
213
+ import DOMPurify from "dompurify";
214
+
215
+ const clean = DOMPurify.sanitize(userSuppliedHtml, {
216
+ ALLOWED_TAGS: ["a", "p", "strong", "em", "ul", "ol", "li"],
217
+ ALLOWED_ATTR: ["href"],
218
+ });
219
+ ```
220
+
221
+ Use sanitization only when users are allowed to provide markup. Plain text should
222
+ be rendered as text, not sanitized and inserted as HTML.
223
+
224
+ ### SQL (Use Parameterized Queries Instead)
225
+
226
+ ```javascript
227
+ // WRONG - never build SQL strings
228
+ const query = `SELECT * FROM users WHERE name = '${userInput}'`;
229
+
230
+ // RIGHT - use parameterized queries
231
+ const query = "SELECT * FROM users WHERE name = ?";
232
+ db.query(query, [userInput]);
233
+ ```
234
+
235
+ ### Path Traversal Prevention
236
+
237
+ ```javascript
238
+ const { constants } = require("node:fs");
239
+ const fs = require("node:fs/promises");
240
+ const path = require("node:path");
241
+
242
+ // Keep baseDir trusted and non-writable by the request user.
243
+ async function openValidatedFile(userPath, baseDir) {
244
+ const baseCanonical = await fs.realpath(baseDir);
245
+ const targetCanonical = await fs.realpath(path.resolve(baseCanonical, userPath));
246
+ const relativePath = path.relative(baseCanonical, targetCanonical);
247
+
248
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
249
+ throw new Error("Path traversal detected");
250
+ }
251
+
252
+ // O_NOFOLLOW rejects a final-component symlink swap. Trusted, non-writable
253
+ // parent directories prevent intermediate-component replacement.
254
+ return fs.open(targetCanonical, constants.O_RDONLY | constants.O_NOFOLLOW);
255
+ }
256
+ ```
257
+
258
+ ## Framework Validation
259
+
260
+ ### Node.js with Joi
261
+
262
+ ```javascript
263
+ const Joi = require("joi");
264
+
265
+ const userSchema = Joi.object({
266
+ username: Joi.string().alphanum().min(3).max(30).required(),
267
+ email: Joi.string().email().required(),
268
+ age: Joi.number().integer().min(0).max(150),
269
+ website: Joi.string().uri({ scheme: ["http", "https"] }),
270
+ });
271
+
272
+ function validateUser(data) {
273
+ const { error, value } = userSchema.validate(data);
274
+ if (error) throw new Error(error.details[0].message);
275
+ return value;
276
+ }
277
+ ```
278
+
279
+ ### Express Validator
280
+
281
+ ```javascript
282
+ const { body, validationResult } = require("express-validator");
283
+
284
+ app.post(
285
+ "/user",
286
+ body("email").isEmail().normalizeEmail(),
287
+ body("password").isLength({ min: 8 }),
288
+ body("age").isInt({ min: 0, max: 150 }),
289
+ (req, res) => {
290
+ const errors = validationResult(req);
291
+ if (!errors.isEmpty()) {
292
+ return res.status(400).json({ errors: errors.array() });
293
+ }
294
+ // Process valid input
295
+ },
296
+ );
297
+ ```
298
+
299
+ ### Zod (TypeScript)
300
+
301
+ ```typescript
302
+ import { z } from "zod";
303
+
304
+ const AllowedWebsiteSchema = z.string().transform((value, ctx) => {
305
+ const url = parseAllowedHttpUrl(value, {
306
+ baseUrl: "https://example.com",
307
+ allowedOrigins: ["https://example.com"],
308
+ allowedPathPrefixes: ["/profiles"],
309
+ });
310
+
311
+ if (!url) {
312
+ ctx.addIssue({ code: "custom", message: "Invalid website URL" });
313
+ return z.NEVER;
314
+ }
315
+
316
+ return url.href;
317
+ });
318
+
319
+ const UserSchema = z.object({
320
+ username: z
321
+ .string()
322
+ .min(3)
323
+ .max(30)
324
+ .regex(/^[a-zA-Z0-9_]+$/),
325
+ email: z.string().email(),
326
+ age: z.number().int().min(0).max(150).optional(),
327
+ website: AllowedWebsiteSchema.optional(),
328
+ });
329
+
330
+ type User = z.infer<typeof UserSchema>;
331
+
332
+ function validateUser(data: unknown): User {
333
+ return UserSchema.parse(data);
334
+ }
335
+ ```
336
+
337
+ Schema validators are useful for structured input and typed parsing, but they do
338
+ not replace business rules. Add refinements or transforms for application rules
339
+ such as allowed URL origins, allowed enum transitions, cross-field constraints,
340
+ and authorization-dependent limits.
341
+
342
+ ## Validation Checklist
343
+
344
+ - [ ] Validate all input on the server
345
+ - [ ] Use allowlist validation when possible
346
+ - [ ] Consume normalized parsed values after validation
347
+ - [ ] Validate data type, length, format, and range
348
+ - [ ] Avoid `parseInt`, `parseFloat`, and free-form `Date.parse()` as validators
349
+ - [ ] Reject unexpected input rather than sanitizing
350
+ - [ ] Use context-specific output encoding at the rendering boundary
351
+ - [ ] Sanitize only when accepting user-supplied HTML fragments
352
+ - [ ] Use parameterized queries for database operations
353
+ - [ ] Validate file uploads (type, size, content)
354
+ - [ ] Canonicalize paths before validation
355
+ - [ ] Log validation failures for monitoring
356
+
357
+ OWASP References:
358
+
359
+ - https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
360
+ - https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html