@schalkneethling/toolkit 0.5.1 → 0.6.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/dist/index.mjs.map +1 -1
- package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
- package/hooks/auto-approve-safe-commands/hook.mts +7 -6
- package/hooks/block-dangerous-commands/hook.mjs +3 -3
- package/hooks/block-dangerous-commands/hook.mts +10 -22
- package/package.json +1 -1
- package/skills/css-tokens/SKILL.md +1 -1
- package/skills/css-tokens/references/tokens.css +6 -10
- package/skills/frontend-security/SKILL.md +3 -0
- package/skills/frontend-security/references/csp-configuration.md +68 -51
- package/skills/frontend-security/references/csrf-protection.md +74 -70
- package/skills/frontend-security/references/dom-security.md +36 -29
- package/skills/frontend-security/references/file-upload-security.md +101 -69
- package/skills/frontend-security/references/framework-patterns.md +42 -40
- package/skills/frontend-security/references/input-validation.md +36 -31
- package/skills/frontend-security/references/jwt-security.md +68 -84
- package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
- package/skills/frontend-security/references/xss-prevention.md +38 -36
- package/skills/frontend-testing/SKILL.md +31 -38
- package/skills/frontend-testing/references/accessibility-testing.md +56 -62
- package/skills/frontend-testing/references/aria-snapshots.md +35 -34
- package/skills/frontend-testing/references/locator-strategies.md +37 -40
- package/skills/frontend-testing/references/visual-regression.md +29 -23
- package/skills/more-secure-dependabot-config/SKILL.md +120 -0
- package/skills/more-secure-dependabot-config/references/ecosystem.md +35 -0
- package/skills/npm-publishing-best-practices/SKILL.md +316 -0
- package/skills/semantic-html/SKILL.md +5 -21
- package/skills/semantic-html/references/heading-patterns.md +1 -5
|
@@ -8,21 +8,21 @@ Generate unique per-session tokens and validate on state-changing requests:
|
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
10
|
// Server-side token generation (Node.js)
|
|
11
|
-
const crypto = require(
|
|
11
|
+
const crypto = require("crypto");
|
|
12
12
|
|
|
13
13
|
function generateCSRFToken(session) {
|
|
14
|
-
const token = crypto.randomBytes(32).toString(
|
|
14
|
+
const token = crypto.randomBytes(32).toString("hex");
|
|
15
15
|
session.csrfToken = token;
|
|
16
16
|
return token;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// Middleware validation
|
|
20
20
|
function validateCSRF(req, res, next) {
|
|
21
|
-
const token = req.headers[
|
|
21
|
+
const token = req.headers["x-csrf-token"] || req.body._csrf;
|
|
22
22
|
const sessionToken = req.session.csrfToken;
|
|
23
23
|
|
|
24
|
-
if (typeof token !==
|
|
25
|
-
return res.status(403).json({ error:
|
|
24
|
+
if (typeof token !== "string" || typeof sessionToken !== "string") {
|
|
25
|
+
return res.status(403).json({ error: "Invalid CSRF token" });
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const tokenBuffer = Buffer.from(token);
|
|
@@ -32,7 +32,7 @@ function validateCSRF(req, res, next) {
|
|
|
32
32
|
tokenBuffer.length !== sessionTokenBuffer.length ||
|
|
33
33
|
!crypto.timingSafeEqual(tokenBuffer, sessionTokenBuffer)
|
|
34
34
|
) {
|
|
35
|
-
return res.status(403).json({ error:
|
|
35
|
+
return res.status(403).json({ error: "Invalid CSRF token" });
|
|
36
36
|
}
|
|
37
37
|
next();
|
|
38
38
|
}
|
|
@@ -42,24 +42,24 @@ function validateCSRF(req, res, next) {
|
|
|
42
42
|
|
|
43
43
|
```javascript
|
|
44
44
|
// Set CSRF cookie
|
|
45
|
-
res.cookie(
|
|
46
|
-
httpOnly: false,
|
|
45
|
+
res.cookie("csrf_token", token, {
|
|
46
|
+
httpOnly: false, // Must be readable by JavaScript
|
|
47
47
|
secure: true,
|
|
48
|
-
sameSite:
|
|
48
|
+
sameSite: "Strict",
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
// Client sends token in header
|
|
52
|
-
fetch(
|
|
53
|
-
method:
|
|
52
|
+
fetch("/api/action", {
|
|
53
|
+
method: "POST",
|
|
54
54
|
headers: {
|
|
55
|
-
|
|
56
|
-
}
|
|
55
|
+
"X-CSRF-Token": getCookie("csrf_token"),
|
|
56
|
+
},
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
// Server validates cookie matches header
|
|
60
60
|
function validateDoubleSubmit(req) {
|
|
61
61
|
const cookieToken = req.cookies.csrf_token;
|
|
62
|
-
const headerToken = req.headers[
|
|
62
|
+
const headerToken = req.headers["x-csrf-token"];
|
|
63
63
|
return cookieToken && cookieToken === headerToken;
|
|
64
64
|
}
|
|
65
65
|
```
|
|
@@ -68,43 +68,43 @@ function validateDoubleSubmit(req) {
|
|
|
68
68
|
|
|
69
69
|
```javascript
|
|
70
70
|
// Strict - never sent cross-site
|
|
71
|
-
res.cookie(
|
|
71
|
+
res.cookie("session", value, { sameSite: "Strict", secure: true, httpOnly: true });
|
|
72
72
|
|
|
73
73
|
// Lax - sent for top-level GET navigations (default in modern browsers)
|
|
74
|
-
res.cookie(
|
|
74
|
+
res.cookie("session", value, { sameSite: "Lax", secure: true, httpOnly: true });
|
|
75
75
|
|
|
76
76
|
// None - requires Secure flag, sent cross-site
|
|
77
|
-
res.cookie(
|
|
77
|
+
res.cookie("session", value, { sameSite: "None", secure: true, httpOnly: true });
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
**Recommendation**: Use `SameSite=Strict` for session cookies when possible, `Lax` as minimum.
|
|
81
81
|
|
|
82
82
|
## Fetch Metadata Headers
|
|
83
83
|
|
|
84
|
-
Validate request origin using Sec-Fetch
|
|
84
|
+
Validate request origin using Sec-Fetch-\* headers:
|
|
85
85
|
|
|
86
86
|
```javascript
|
|
87
87
|
function validateFetchMetadata(req, res, next) {
|
|
88
|
-
const site = req.headers[
|
|
89
|
-
const mode = req.headers[
|
|
90
|
-
const dest = req.headers[
|
|
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
91
|
const method = req.method;
|
|
92
92
|
|
|
93
93
|
// Allow same-origin requests
|
|
94
|
-
if (site ===
|
|
94
|
+
if (site === "same-origin") return next();
|
|
95
95
|
|
|
96
96
|
// Allow user-initiated browser navigations
|
|
97
|
-
if (site ===
|
|
97
|
+
if (site === "none") return next();
|
|
98
98
|
|
|
99
99
|
// Allow same-site top-level navigations and safe methods
|
|
100
100
|
if (
|
|
101
|
-
site ===
|
|
102
|
-
(mode ===
|
|
101
|
+
site === "same-site" &&
|
|
102
|
+
(mode === "navigate" || ["GET", "HEAD", "OPTIONS"].includes(method))
|
|
103
103
|
) {
|
|
104
104
|
return next();
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
return res.status(403).json({ error:
|
|
107
|
+
return res.status(403).json({ error: "Fetch metadata validation failed" });
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
@@ -113,20 +113,24 @@ function validateFetchMetadata(req, res, next) {
|
|
|
113
113
|
### Express.js with Signed Double-Submit Tokens
|
|
114
114
|
|
|
115
115
|
```javascript
|
|
116
|
-
const crypto = require(
|
|
116
|
+
const crypto = require("crypto");
|
|
117
|
+
|
|
118
|
+
const CSRF_COOKIE_NAME = "csrf_token";
|
|
119
|
+
if (!/^[a-f0-9]{64,}$/i.test(process.env.CSRF_SECRET || "")) {
|
|
120
|
+
throw new Error("CSRF_SECRET must be a hex-encoded secret with at least 32 bytes of entropy");
|
|
121
|
+
}
|
|
117
122
|
|
|
118
|
-
const
|
|
119
|
-
const CSRF_SECRET = Buffer.from(process.env.CSRF_SECRET, 'hex');
|
|
123
|
+
const CSRF_SECRET = Buffer.from(process.env.CSRF_SECRET, "hex");
|
|
120
124
|
|
|
121
125
|
function signToken(sessionId, nonce) {
|
|
122
126
|
return crypto
|
|
123
|
-
.createHmac(
|
|
127
|
+
.createHmac("sha256", CSRF_SECRET)
|
|
124
128
|
.update(`${sessionId}:${nonce}`)
|
|
125
|
-
.digest(
|
|
129
|
+
.digest("base64url");
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
function constantTimeEqual(value, expected) {
|
|
129
|
-
if (typeof value !==
|
|
133
|
+
if (typeof value !== "string" || typeof expected !== "string") return false;
|
|
130
134
|
|
|
131
135
|
const valueBuffer = Buffer.from(value);
|
|
132
136
|
const expectedBuffer = Buffer.from(expected);
|
|
@@ -138,7 +142,7 @@ function constantTimeEqual(value, expected) {
|
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function generateToken(req) {
|
|
141
|
-
const nonce = crypto.randomBytes(32).toString(
|
|
145
|
+
const nonce = crypto.randomBytes(32).toString("base64url");
|
|
142
146
|
const signature = signToken(req.session.id, nonce);
|
|
143
147
|
return `${nonce}.${signature}`;
|
|
144
148
|
}
|
|
@@ -149,7 +153,7 @@ function sendToken(req, res, next) {
|
|
|
149
153
|
res.cookie(CSRF_COOKIE_NAME, token, {
|
|
150
154
|
httpOnly: true,
|
|
151
155
|
secure: true,
|
|
152
|
-
sameSite:
|
|
156
|
+
sameSite: "Strict",
|
|
153
157
|
});
|
|
154
158
|
|
|
155
159
|
res.locals.csrfToken = token;
|
|
@@ -157,30 +161,30 @@ function sendToken(req, res, next) {
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
function verifyToken(req, res, next) {
|
|
160
|
-
const token = req.headers[
|
|
164
|
+
const token = req.headers["x-csrf-token"] || req.body._csrf;
|
|
161
165
|
const cookieToken = req.cookies[CSRF_COOKIE_NAME];
|
|
162
166
|
|
|
163
167
|
if (!constantTimeEqual(token, cookieToken)) {
|
|
164
|
-
return res.status(403).json({ error:
|
|
168
|
+
return res.status(403).json({ error: "Invalid CSRF token" });
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
const [nonce, signature, extra] = cookieToken.split(
|
|
171
|
+
const [nonce, signature, extra] = cookieToken.split(".");
|
|
168
172
|
if (extra || !nonce || !signature) {
|
|
169
|
-
return res.status(403).json({ error:
|
|
173
|
+
return res.status(403).json({ error: "Invalid CSRF token" });
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
if (!constantTimeEqual(signature, signToken(req.session.id, nonce))) {
|
|
173
|
-
return res.status(403).json({ error:
|
|
177
|
+
return res.status(403).json({ error: "Invalid CSRF token" });
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
next();
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
app.get(
|
|
180
|
-
res.render(
|
|
183
|
+
app.get("/form", sendToken, (req, res) => {
|
|
184
|
+
res.render("form", { csrfToken: res.locals.csrfToken });
|
|
181
185
|
});
|
|
182
186
|
|
|
183
|
-
app.post(
|
|
187
|
+
app.post("/submit", verifyToken, (req, res) => {
|
|
184
188
|
res.json({ ok: true });
|
|
185
189
|
});
|
|
186
190
|
```
|
|
@@ -188,16 +192,16 @@ app.post('/submit', verifyToken, (req, res) => {
|
|
|
188
192
|
Example tests for token issuance and verification:
|
|
189
193
|
|
|
190
194
|
```javascript
|
|
191
|
-
const assert = require(
|
|
192
|
-
const test = require(
|
|
195
|
+
const assert = require("node:assert/strict");
|
|
196
|
+
const test = require("node:test");
|
|
193
197
|
|
|
194
|
-
test(
|
|
195
|
-
const req = { session: { id:
|
|
198
|
+
test("sendToken issues a cookie and exposes a form token", () => {
|
|
199
|
+
const req = { session: { id: "session-123" } };
|
|
196
200
|
const res = {
|
|
197
201
|
locals: {},
|
|
198
202
|
cookie(name, value, options) {
|
|
199
203
|
this.cookieArgs = { name, value, options };
|
|
200
|
-
}
|
|
204
|
+
},
|
|
201
205
|
};
|
|
202
206
|
|
|
203
207
|
sendToken(req, res, () => {});
|
|
@@ -205,36 +209,36 @@ test('sendToken issues a cookie and exposes a form token', () => {
|
|
|
205
209
|
assert.equal(res.cookieArgs.name, CSRF_COOKIE_NAME);
|
|
206
210
|
assert.equal(res.locals.csrfToken, res.cookieArgs.value);
|
|
207
211
|
assert.equal(res.cookieArgs.options.httpOnly, true);
|
|
208
|
-
assert.equal(res.cookieArgs.options.sameSite,
|
|
212
|
+
assert.equal(res.cookieArgs.options.sameSite, "Strict");
|
|
209
213
|
});
|
|
210
214
|
|
|
211
|
-
test(
|
|
212
|
-
const req = { session: { id:
|
|
215
|
+
test("verifyToken accepts a matching signed token", () => {
|
|
216
|
+
const req = { session: { id: "session-123" } };
|
|
213
217
|
const token = generateToken(req);
|
|
214
218
|
let called = false;
|
|
215
219
|
|
|
216
220
|
verifyToken(
|
|
217
221
|
{
|
|
218
222
|
...req,
|
|
219
|
-
headers: {
|
|
223
|
+
headers: { "x-csrf-token": token },
|
|
220
224
|
body: {},
|
|
221
|
-
cookies: { [CSRF_COOKIE_NAME]: token }
|
|
225
|
+
cookies: { [CSRF_COOKIE_NAME]: token },
|
|
222
226
|
},
|
|
223
227
|
{},
|
|
224
228
|
() => {
|
|
225
229
|
called = true;
|
|
226
|
-
}
|
|
230
|
+
},
|
|
227
231
|
);
|
|
228
232
|
|
|
229
233
|
assert.equal(called, true);
|
|
230
234
|
});
|
|
231
235
|
|
|
232
|
-
test(
|
|
236
|
+
test("verifyToken rejects mismatched or tampered tokens", () => {
|
|
233
237
|
const req = {
|
|
234
|
-
session: { id:
|
|
235
|
-
headers: {
|
|
238
|
+
session: { id: "session-123" },
|
|
239
|
+
headers: { "x-csrf-token": "tampered.token" },
|
|
236
240
|
body: {},
|
|
237
|
-
cookies: { [CSRF_COOKIE_NAME]: generateToken({ session: { id:
|
|
241
|
+
cookies: { [CSRF_COOKIE_NAME]: generateToken({ session: { id: "session-123" } }) },
|
|
238
242
|
};
|
|
239
243
|
const res = {
|
|
240
244
|
status(code) {
|
|
@@ -243,13 +247,13 @@ test('verifyToken rejects mismatched or tampered tokens', () => {
|
|
|
243
247
|
},
|
|
244
248
|
json(body) {
|
|
245
249
|
this.body = body;
|
|
246
|
-
}
|
|
250
|
+
},
|
|
247
251
|
};
|
|
248
252
|
|
|
249
253
|
verifyToken(req, res, () => {});
|
|
250
254
|
|
|
251
255
|
assert.equal(res.statusCode, 403);
|
|
252
|
-
assert.deepEqual(res.body, { error:
|
|
256
|
+
assert.deepEqual(res.body, { error: "Invalid CSRF token" });
|
|
253
257
|
});
|
|
254
258
|
```
|
|
255
259
|
|
|
@@ -259,13 +263,13 @@ test('verifyToken rejects mismatched or tampered tokens', () => {
|
|
|
259
263
|
function Form({ csrfToken }) {
|
|
260
264
|
const handleSubmit = async (e) => {
|
|
261
265
|
e.preventDefault();
|
|
262
|
-
await fetch(
|
|
263
|
-
method:
|
|
266
|
+
await fetch("/api/submit", {
|
|
267
|
+
method: "POST",
|
|
264
268
|
headers: {
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
"Content-Type": "application/json",
|
|
270
|
+
"X-CSRF-Token": csrfToken,
|
|
267
271
|
},
|
|
268
|
-
body: JSON.stringify(formData)
|
|
272
|
+
body: JSON.stringify(formData),
|
|
269
273
|
});
|
|
270
274
|
};
|
|
271
275
|
|
|
@@ -293,10 +297,10 @@ Protect against CSRF in single-page applications:
|
|
|
293
297
|
|
|
294
298
|
```javascript
|
|
295
299
|
// Set up axios defaults
|
|
296
|
-
import axios from
|
|
300
|
+
import axios from "axios";
|
|
297
301
|
|
|
298
|
-
axios.defaults.xsrfCookieName =
|
|
299
|
-
axios.defaults.xsrfHeaderName =
|
|
302
|
+
axios.defaults.xsrfCookieName = "csrf_token";
|
|
303
|
+
axios.defaults.xsrfHeaderName = "X-CSRF-Token";
|
|
300
304
|
axios.defaults.withCredentials = true;
|
|
301
305
|
|
|
302
306
|
// Or with fetch
|
|
@@ -305,11 +309,11 @@ async function secureFetch(url, options = {}) {
|
|
|
305
309
|
|
|
306
310
|
return fetch(url, {
|
|
307
311
|
...options,
|
|
308
|
-
credentials:
|
|
312
|
+
credentials: "same-origin",
|
|
309
313
|
headers: {
|
|
310
314
|
...options.headers,
|
|
311
|
-
|
|
312
|
-
}
|
|
315
|
+
"X-CSRF-Token": csrfToken,
|
|
316
|
+
},
|
|
313
317
|
});
|
|
314
318
|
}
|
|
315
319
|
```
|
|
@@ -8,13 +8,13 @@ Untrusted data in HTML element content:
|
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
10
|
// UNSAFE
|
|
11
|
-
element.innerHTML =
|
|
11
|
+
element.innerHTML = "<div>" + userInput + "</div>";
|
|
12
12
|
|
|
13
13
|
// SAFE - use textContent
|
|
14
14
|
element.textContent = userInput;
|
|
15
15
|
|
|
16
16
|
// SAFE - create elements programmatically
|
|
17
|
-
const div = document.createElement(
|
|
17
|
+
const div = document.createElement("div");
|
|
18
18
|
div.textContent = userInput;
|
|
19
19
|
parent.appendChild(div);
|
|
20
20
|
```
|
|
@@ -39,14 +39,14 @@ const value = element.dataset.value;
|
|
|
39
39
|
|
|
40
40
|
```javascript
|
|
41
41
|
// UNSAFE - event handlers
|
|
42
|
-
element.setAttribute(
|
|
42
|
+
element.setAttribute("onclick", userInput);
|
|
43
43
|
|
|
44
44
|
// UNSAFE - dangerous attributes
|
|
45
|
-
element.setAttribute(
|
|
46
|
-
element.setAttribute(
|
|
45
|
+
element.setAttribute("href", userInput); // javascript: URLs
|
|
46
|
+
element.setAttribute("src", userInput); // script injection
|
|
47
47
|
|
|
48
48
|
// SAFE - safe attributes only
|
|
49
|
-
const safeAttributes = [
|
|
49
|
+
const safeAttributes = ["title", "alt", "class", "id", "name"];
|
|
50
50
|
if (safeAttributes.includes(attributeName)) {
|
|
51
51
|
element.setAttribute(attributeName, userInput);
|
|
52
52
|
}
|
|
@@ -57,7 +57,7 @@ if (safeAttributes.includes(attributeName)) {
|
|
|
57
57
|
```javascript
|
|
58
58
|
// UNSAFE - expression injection
|
|
59
59
|
element.style.cssText = userInput;
|
|
60
|
-
element.setAttribute(
|
|
60
|
+
element.setAttribute("style", userInput);
|
|
61
61
|
|
|
62
62
|
// SAFE - set specific properties
|
|
63
63
|
element.style.backgroundColor = sanitizeColor(userInput);
|
|
@@ -65,7 +65,7 @@ element.style.backgroundColor = sanitizeColor(userInput);
|
|
|
65
65
|
function sanitizeColor(input) {
|
|
66
66
|
// Only allow safe color values
|
|
67
67
|
const colorRegex = /^#[0-9A-Fa-f]{6}$|^#[0-9A-Fa-f]{3}$|^rgb\(\d{1,3},\s*\d{1,3},\s*\d{1,3}\)$/;
|
|
68
|
-
return colorRegex.test(input) ? input :
|
|
68
|
+
return colorRegex.test(input) ? input : "inherit";
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -75,13 +75,13 @@ function sanitizeColor(input) {
|
|
|
75
75
|
// UNSAFE - unvalidated URLs
|
|
76
76
|
location.href = userInput;
|
|
77
77
|
window.open(userInput);
|
|
78
|
-
element.setAttribute(
|
|
78
|
+
element.setAttribute("href", userInput);
|
|
79
79
|
|
|
80
80
|
// SAFE - validate URL protocol
|
|
81
81
|
function validateUrl(input) {
|
|
82
82
|
try {
|
|
83
83
|
const url = new URL(input, window.location.origin);
|
|
84
|
-
const allowedProtocols = [
|
|
84
|
+
const allowedProtocols = ["http:", "https:", "mailto:"];
|
|
85
85
|
if (!allowedProtocols.includes(url.protocol)) {
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
@@ -105,7 +105,7 @@ Named elements (id, name attributes) create global variables:
|
|
|
105
105
|
|
|
106
106
|
```html
|
|
107
107
|
<!-- This creates window.config -->
|
|
108
|
-
<img id="config" src="x"
|
|
108
|
+
<img id="config" src="x" />
|
|
109
109
|
|
|
110
110
|
<!-- JavaScript that assumes config is an object will break -->
|
|
111
111
|
<script>
|
|
@@ -117,12 +117,17 @@ Named elements (id, name attributes) create global variables:
|
|
|
117
117
|
|
|
118
118
|
```javascript
|
|
119
119
|
// 1. Use Object.hasOwn() or hasOwnProperty()
|
|
120
|
-
if (
|
|
121
|
-
|
|
120
|
+
if (
|
|
121
|
+
Object.hasOwn(window, "config") &&
|
|
122
|
+
typeof window.config === "object" &&
|
|
123
|
+
window.config !== null &&
|
|
124
|
+
!(window.config instanceof Element)
|
|
125
|
+
) {
|
|
126
|
+
// Safe to use window.config
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
// 2. Access through document methods
|
|
125
|
-
const element = document.getElementById(
|
|
130
|
+
const element = document.getElementById("config");
|
|
126
131
|
|
|
127
132
|
// 3. Use Map instead of objects for user-controlled keys
|
|
128
133
|
const userConfig = new Map();
|
|
@@ -133,7 +138,7 @@ Object.freeze(window.config);
|
|
|
133
138
|
|
|
134
139
|
// 5. Use nullish coalescing with type checking
|
|
135
140
|
const config = window.config ?? {};
|
|
136
|
-
if (typeof config.apiKey ===
|
|
141
|
+
if (typeof config.apiKey === "string") {
|
|
137
142
|
// Safe to use
|
|
138
143
|
}
|
|
139
144
|
```
|
|
@@ -143,8 +148,8 @@ if (typeof config.apiKey === 'string') {
|
|
|
143
148
|
```javascript
|
|
144
149
|
// DOMPurify with clobbering protection
|
|
145
150
|
const clean = DOMPurify.sanitize(dirty, {
|
|
146
|
-
SANITIZE_DOM: true,
|
|
147
|
-
SANITIZE_NAMED_PROPS: true
|
|
151
|
+
SANITIZE_DOM: true, // Remove clobbering vectors
|
|
152
|
+
SANITIZE_NAMED_PROPS: true,
|
|
148
153
|
});
|
|
149
154
|
```
|
|
150
155
|
|
|
@@ -158,7 +163,7 @@ element.textContent = userInput;
|
|
|
158
163
|
document.createTextNode(userInput);
|
|
159
164
|
|
|
160
165
|
// Attribute manipulation (for safe attributes)
|
|
161
|
-
element.setAttribute(
|
|
166
|
+
element.setAttribute("data-value", userInput);
|
|
162
167
|
element.classList.add(sanitizedClass);
|
|
163
168
|
|
|
164
169
|
// Query selectors (read-only)
|
|
@@ -173,11 +178,11 @@ document.querySelectorAll(selector);
|
|
|
173
178
|
element.innerHTML = sanitized;
|
|
174
179
|
element.outerHTML = sanitized;
|
|
175
180
|
element.insertAdjacentHTML(position, sanitized);
|
|
176
|
-
document.write(sanitized);
|
|
181
|
+
document.write(sanitized); // Avoid entirely
|
|
177
182
|
|
|
178
183
|
// Script execution
|
|
179
|
-
eval();
|
|
180
|
-
new Function();
|
|
184
|
+
eval(); // Never use with user input
|
|
185
|
+
new Function(); // Never use with user input
|
|
181
186
|
setTimeout(string); // Never pass strings
|
|
182
187
|
setInterval(string); // Never pass strings
|
|
183
188
|
```
|
|
@@ -186,17 +191,17 @@ setInterval(string); // Never pass strings
|
|
|
186
191
|
|
|
187
192
|
```javascript
|
|
188
193
|
// Sender - specify exact origin
|
|
189
|
-
targetWindow.postMessage(data,
|
|
194
|
+
targetWindow.postMessage(data, "https://trusted-domain.com");
|
|
190
195
|
|
|
191
196
|
// Receiver - always validate origin
|
|
192
|
-
window.addEventListener(
|
|
197
|
+
window.addEventListener("message", (event) => {
|
|
193
198
|
// Validate origin
|
|
194
|
-
if (event.origin !==
|
|
199
|
+
if (event.origin !== "https://trusted-domain.com") {
|
|
195
200
|
return;
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
// Validate data structure
|
|
199
|
-
if (typeof event.data !==
|
|
204
|
+
if (event.data === null || typeof event.data !== "object" || !event.data.type) {
|
|
200
205
|
return;
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -212,14 +217,16 @@ window.addEventListener('message', (event) => {
|
|
|
212
217
|
// Content-Security-Policy: require-trusted-types-for 'script'
|
|
213
218
|
|
|
214
219
|
// Create a policy
|
|
215
|
-
const policy = trustedTypes.createPolicy(
|
|
220
|
+
const policy = trustedTypes.createPolicy("default", {
|
|
216
221
|
createHTML: (input) => DOMPurify.sanitize(input),
|
|
217
|
-
createScript: () => {
|
|
222
|
+
createScript: () => {
|
|
223
|
+
throw new Error("Scripts not allowed");
|
|
224
|
+
},
|
|
218
225
|
createScriptURL: (input) => {
|
|
219
226
|
const url = new URL(input, location.origin);
|
|
220
227
|
if (url.origin === location.origin) return input;
|
|
221
|
-
throw new Error(
|
|
222
|
-
}
|
|
228
|
+
throw new Error("Invalid script URL");
|
|
229
|
+
},
|
|
223
230
|
});
|
|
224
231
|
|
|
225
232
|
// Usage
|