@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
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
```javascript
|
|
20
20
|
const ALLOWED_EXTENSIONS = {
|
|
21
|
-
images: [
|
|
22
|
-
documents: [
|
|
23
|
-
data: [
|
|
21
|
+
images: [".jpg", ".jpeg", ".png", ".gif", ".webp"],
|
|
22
|
+
documents: [".pdf", ".docx", ".xlsx"],
|
|
23
|
+
data: [".csv", ".json"],
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
function validateExtension(filename, category) {
|
|
@@ -34,22 +34,50 @@ function validateExtension(filename, category) {
|
|
|
34
34
|
```javascript
|
|
35
35
|
const DANGEROUS_EXTENSIONS = [
|
|
36
36
|
// Server-side execution
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
".php",
|
|
38
|
+
".php3",
|
|
39
|
+
".php4",
|
|
40
|
+
".php5",
|
|
41
|
+
".phtml",
|
|
42
|
+
".asp",
|
|
43
|
+
".aspx",
|
|
44
|
+
".ascx",
|
|
45
|
+
".ashx",
|
|
46
|
+
".jsp",
|
|
47
|
+
".jspx",
|
|
48
|
+
".jspa",
|
|
49
|
+
".cgi",
|
|
50
|
+
".pl",
|
|
51
|
+
".py",
|
|
52
|
+
".rb",
|
|
41
53
|
|
|
42
54
|
// Windows executable
|
|
43
|
-
|
|
55
|
+
".exe",
|
|
56
|
+
".dll",
|
|
57
|
+
".bat",
|
|
58
|
+
".cmd",
|
|
59
|
+
".com",
|
|
60
|
+
".msi",
|
|
61
|
+
".ps1",
|
|
44
62
|
|
|
45
63
|
// Script files
|
|
46
|
-
|
|
64
|
+
".js",
|
|
65
|
+
".vbs",
|
|
66
|
+
".wsf",
|
|
67
|
+
".hta",
|
|
47
68
|
|
|
48
69
|
// Config files
|
|
49
|
-
|
|
70
|
+
".htaccess",
|
|
71
|
+
".htpasswd",
|
|
72
|
+
".config",
|
|
73
|
+
".ini",
|
|
50
74
|
|
|
51
75
|
// Archive (can contain malicious files)
|
|
52
|
-
|
|
76
|
+
".zip",
|
|
77
|
+
".tar",
|
|
78
|
+
".gz",
|
|
79
|
+
".rar",
|
|
80
|
+
".7z",
|
|
53
81
|
];
|
|
54
82
|
```
|
|
55
83
|
|
|
@@ -58,7 +86,7 @@ const DANGEROUS_EXTENSIONS = [
|
|
|
58
86
|
```javascript
|
|
59
87
|
function sanitizeFilename(filename) {
|
|
60
88
|
// Remove all extensions except the last
|
|
61
|
-
const parts = filename.split(
|
|
89
|
+
const parts = filename.split(".");
|
|
62
90
|
if (parts.length > 2) {
|
|
63
91
|
return `${parts[0]}.${parts[parts.length - 1]}`;
|
|
64
92
|
}
|
|
@@ -74,13 +102,13 @@ function sanitizeFilename(filename) {
|
|
|
74
102
|
|
|
75
103
|
```javascript
|
|
76
104
|
const ALLOWED_MIME_TYPES = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
".jpg": ["image/jpeg"],
|
|
106
|
+
".jpeg": ["image/jpeg"],
|
|
107
|
+
".png": ["image/png"],
|
|
108
|
+
".gif": ["image/gif"],
|
|
109
|
+
".webp": ["image/webp"],
|
|
110
|
+
".pdf": ["application/pdf"],
|
|
111
|
+
".docx": ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
|
84
112
|
};
|
|
85
113
|
|
|
86
114
|
function validateMimeType(file) {
|
|
@@ -96,18 +124,21 @@ function validateMimeType(file) {
|
|
|
96
124
|
|
|
97
125
|
```javascript
|
|
98
126
|
const FILE_SIGNATURES = {
|
|
99
|
-
jpg: Buffer.from([
|
|
100
|
-
png: Buffer.from([0x89, 0x50,
|
|
127
|
+
jpg: Buffer.from([0xff, 0xd8, 0xff]),
|
|
128
|
+
png: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
101
129
|
gif: Buffer.from([0x47, 0x49, 0x46, 0x38]),
|
|
102
130
|
pdf: Buffer.from([0x25, 0x50, 0x44, 0x46]),
|
|
103
|
-
zip: Buffer.from([0x50,
|
|
131
|
+
zip: Buffer.from([0x50, 0x4b, 0x03, 0x04]),
|
|
104
132
|
};
|
|
105
133
|
|
|
106
134
|
async function validateFileSignature(filePath, expectedType) {
|
|
107
135
|
const buffer = Buffer.alloc(8);
|
|
108
|
-
const fd = await fs.open(filePath,
|
|
109
|
-
|
|
110
|
-
|
|
136
|
+
const fd = await fs.open(filePath, "r");
|
|
137
|
+
try {
|
|
138
|
+
await fd.read(buffer, 0, 8, 0);
|
|
139
|
+
} finally {
|
|
140
|
+
await fd.close();
|
|
141
|
+
}
|
|
111
142
|
|
|
112
143
|
const signature = FILE_SIGNATURES[expectedType];
|
|
113
144
|
if (!signature) return false;
|
|
@@ -119,17 +150,17 @@ async function validateFileSignature(filePath, expectedType) {
|
|
|
119
150
|
## Safe Storage
|
|
120
151
|
|
|
121
152
|
```javascript
|
|
122
|
-
const multer = require(
|
|
123
|
-
const path = require(
|
|
124
|
-
const crypto = require(
|
|
153
|
+
const multer = require("multer");
|
|
154
|
+
const path = require("path");
|
|
155
|
+
const crypto = require("crypto");
|
|
125
156
|
|
|
126
157
|
// Store OUTSIDE webroot
|
|
127
|
-
const UPLOAD_DIR =
|
|
158
|
+
const UPLOAD_DIR = "/var/app/uploads"; // Not in /public/
|
|
128
159
|
|
|
129
160
|
const storage = multer.diskStorage({
|
|
130
161
|
destination: (req, file, cb) => {
|
|
131
162
|
// Organize by date
|
|
132
|
-
const date = new Date().toISOString().split(
|
|
163
|
+
const date = new Date().toISOString().split("T")[0];
|
|
133
164
|
const dir = path.join(UPLOAD_DIR, date);
|
|
134
165
|
fs.mkdirSync(dir, { recursive: true });
|
|
135
166
|
cb(null, dir);
|
|
@@ -137,24 +168,24 @@ const storage = multer.diskStorage({
|
|
|
137
168
|
filename: (req, file, cb) => {
|
|
138
169
|
// Generate random filename
|
|
139
170
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
140
|
-
const name = crypto.randomBytes(16).toString(
|
|
171
|
+
const name = crypto.randomBytes(16).toString("hex");
|
|
141
172
|
cb(null, `${name}${ext}`);
|
|
142
|
-
}
|
|
173
|
+
},
|
|
143
174
|
});
|
|
144
175
|
|
|
145
176
|
const upload = multer({
|
|
146
177
|
storage,
|
|
147
178
|
limits: {
|
|
148
|
-
fileSize: 5 * 1024 * 1024,
|
|
149
|
-
files: 1
|
|
179
|
+
fileSize: 5 * 1024 * 1024, // 5MB
|
|
180
|
+
files: 1,
|
|
150
181
|
},
|
|
151
182
|
fileFilter: (req, file, cb) => {
|
|
152
183
|
if (!validateMimeType(file)) {
|
|
153
|
-
cb(new Error(
|
|
184
|
+
cb(new Error("Invalid file type"));
|
|
154
185
|
return;
|
|
155
186
|
}
|
|
156
187
|
cb(null, true);
|
|
157
|
-
}
|
|
188
|
+
},
|
|
158
189
|
});
|
|
159
190
|
```
|
|
160
191
|
|
|
@@ -162,20 +193,20 @@ const upload = multer({
|
|
|
162
193
|
|
|
163
194
|
```javascript
|
|
164
195
|
// Serve files through application, not directly
|
|
165
|
-
app.get(
|
|
196
|
+
app.get("/files/:id", async (req, res) => {
|
|
166
197
|
// Verify user authorization
|
|
167
198
|
if (!req.user || !canAccessFile(req.user, req.params.id)) {
|
|
168
|
-
return res.status(403).send(
|
|
199
|
+
return res.status(403).send("Forbidden");
|
|
169
200
|
}
|
|
170
201
|
|
|
171
202
|
// Get file from database (not from user input)
|
|
172
203
|
const fileRecord = await db.getFile(req.params.id);
|
|
173
|
-
if (!fileRecord) return res.status(404).send(
|
|
204
|
+
if (!fileRecord) return res.status(404).send("Not found");
|
|
174
205
|
|
|
175
206
|
// Set safe headers
|
|
176
|
-
res.setHeader(
|
|
177
|
-
res.setHeader(
|
|
178
|
-
res.setHeader(
|
|
207
|
+
res.setHeader("Content-Type", fileRecord.mimeType);
|
|
208
|
+
res.setHeader("Content-Disposition", `attachment; filename="${fileRecord.safeName}"`);
|
|
209
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
179
210
|
|
|
180
211
|
// Stream file
|
|
181
212
|
const stream = fs.createReadStream(fileRecord.path);
|
|
@@ -188,12 +219,12 @@ app.get('/files/:id', async (req, res) => {
|
|
|
188
219
|
Destroy potential malicious content by re-encoding images:
|
|
189
220
|
|
|
190
221
|
```javascript
|
|
191
|
-
const sharp = require(
|
|
222
|
+
const sharp = require("sharp");
|
|
192
223
|
|
|
193
224
|
async function sanitizeImage(inputPath, outputPath) {
|
|
194
225
|
await sharp(inputPath)
|
|
195
|
-
.rotate()
|
|
196
|
-
.toFormat(
|
|
226
|
+
.rotate() // Apply EXIF orientation
|
|
227
|
+
.toFormat("jpeg", { quality: 90 }) // Re-encode
|
|
197
228
|
.toFile(outputPath);
|
|
198
229
|
}
|
|
199
230
|
```
|
|
@@ -201,8 +232,8 @@ async function sanitizeImage(inputPath, outputPath) {
|
|
|
201
232
|
## ZIP File Handling
|
|
202
233
|
|
|
203
234
|
```javascript
|
|
204
|
-
const AdmZip = require(
|
|
205
|
-
const path = require(
|
|
235
|
+
const AdmZip = require("adm-zip");
|
|
236
|
+
const path = require("path");
|
|
206
237
|
|
|
207
238
|
function safeExtractZip(zipPath, destDir, maxSize = 100 * 1024 * 1024) {
|
|
208
239
|
const zip = new AdmZip(zipPath);
|
|
@@ -216,20 +247,20 @@ function safeExtractZip(zipPath, destDir, maxSize = 100 * 1024 * 1024) {
|
|
|
216
247
|
const resolvedEntry = path.resolve(destDir, entry.entryName);
|
|
217
248
|
const relativeEntry = path.relative(resolvedDest, resolvedEntry);
|
|
218
249
|
|
|
219
|
-
if (relativeEntry.startsWith(
|
|
220
|
-
throw new Error(
|
|
250
|
+
if (relativeEntry.startsWith("..") || path.isAbsolute(relativeEntry)) {
|
|
251
|
+
throw new Error("Path traversal detected");
|
|
221
252
|
}
|
|
222
253
|
|
|
223
254
|
// Check for zip bomb
|
|
224
255
|
totalSize += entry.header.size;
|
|
225
256
|
if (totalSize > maxSize) {
|
|
226
|
-
throw new Error(
|
|
257
|
+
throw new Error("Extracted size exceeds limit");
|
|
227
258
|
}
|
|
228
259
|
|
|
229
260
|
// Check compression ratio (zip bomb indicator)
|
|
230
261
|
const ratio = entry.header.size / entry.header.compressedSize;
|
|
231
262
|
if (ratio > 100) {
|
|
232
|
-
throw new Error(
|
|
263
|
+
throw new Error("Suspicious compression ratio");
|
|
233
264
|
}
|
|
234
265
|
}
|
|
235
266
|
|
|
@@ -240,18 +271,18 @@ function safeExtractZip(zipPath, destDir, maxSize = 100 * 1024 * 1024) {
|
|
|
240
271
|
## Express.js Complete Example
|
|
241
272
|
|
|
242
273
|
```javascript
|
|
243
|
-
const express = require(
|
|
244
|
-
const multer = require(
|
|
245
|
-
const path = require(
|
|
246
|
-
const crypto = require(
|
|
247
|
-
const fs = require(
|
|
274
|
+
const express = require("express");
|
|
275
|
+
const multer = require("multer");
|
|
276
|
+
const path = require("path");
|
|
277
|
+
const crypto = require("crypto");
|
|
278
|
+
const fs = require("fs").promises;
|
|
248
279
|
|
|
249
280
|
const app = express();
|
|
250
281
|
|
|
251
282
|
// Configuration
|
|
252
|
-
const UPLOAD_DIR =
|
|
283
|
+
const UPLOAD_DIR = "/var/app/uploads";
|
|
253
284
|
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
254
|
-
const ALLOWED_TYPES = [
|
|
285
|
+
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/gif"];
|
|
255
286
|
|
|
256
287
|
// Multer setup
|
|
257
288
|
const upload = multer({
|
|
@@ -259,26 +290,27 @@ const upload = multer({
|
|
|
259
290
|
limits: { fileSize: MAX_FILE_SIZE },
|
|
260
291
|
fileFilter: (req, file, cb) => {
|
|
261
292
|
if (!ALLOWED_TYPES.includes(file.mimetype)) {
|
|
262
|
-
cb(new multer.MulterError(
|
|
293
|
+
cb(new multer.MulterError("LIMIT_UNEXPECTED_FILE"));
|
|
263
294
|
return;
|
|
264
295
|
}
|
|
265
296
|
cb(null, true);
|
|
266
|
-
}
|
|
297
|
+
},
|
|
267
298
|
});
|
|
268
299
|
|
|
269
300
|
// Upload endpoint
|
|
270
|
-
app.post(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
301
|
+
app.post(
|
|
302
|
+
"/upload",
|
|
303
|
+
requireAuth, // Authentication
|
|
304
|
+
verifyToken, // CSRF token
|
|
305
|
+
upload.single("file"), // File handling
|
|
274
306
|
async (req, res) => {
|
|
275
307
|
try {
|
|
276
308
|
const file = req.file;
|
|
277
|
-
if (!file) return res.status(400).json({ error:
|
|
309
|
+
if (!file) return res.status(400).json({ error: "No file" });
|
|
278
310
|
|
|
279
311
|
// Validate magic bytes
|
|
280
312
|
if (!validateMagicBytes(file.buffer, file.mimetype)) {
|
|
281
|
-
return res.status(400).json({ error:
|
|
313
|
+
return res.status(400).json({ error: "Invalid file" });
|
|
282
314
|
}
|
|
283
315
|
|
|
284
316
|
// Generate safe filename
|
|
@@ -295,15 +327,15 @@ app.post('/upload',
|
|
|
295
327
|
filename,
|
|
296
328
|
originalName: file.originalname,
|
|
297
329
|
mimeType: file.mimetype,
|
|
298
|
-
size: file.size
|
|
330
|
+
size: file.size,
|
|
299
331
|
});
|
|
300
332
|
|
|
301
333
|
res.json({ id: fileRecord.id });
|
|
302
334
|
} catch (error) {
|
|
303
335
|
console.error(error);
|
|
304
|
-
res.status(500).json({ error:
|
|
336
|
+
res.status(500).json({ error: "Upload failed" });
|
|
305
337
|
}
|
|
306
|
-
}
|
|
338
|
+
},
|
|
307
339
|
);
|
|
308
340
|
```
|
|
309
341
|
|
|
@@ -20,18 +20,18 @@ import DOMPurify from 'dompurify';
|
|
|
20
20
|
|
|
21
21
|
```jsx
|
|
22
22
|
// DANGEROUS - javascript: URLs in href
|
|
23
|
-
<a href={userInput}>Link</a
|
|
23
|
+
<a href={userInput}>Link</a>;
|
|
24
24
|
|
|
25
25
|
// SAFE - validate URL protocol
|
|
26
26
|
function SafeLink({ href, children }) {
|
|
27
27
|
const safeHref = useMemo(() => {
|
|
28
28
|
try {
|
|
29
29
|
const url = new URL(href, window.location.origin);
|
|
30
|
-
if ([
|
|
30
|
+
if (["http:", "https:", "mailto:"].includes(url.protocol)) {
|
|
31
31
|
return href;
|
|
32
32
|
}
|
|
33
33
|
} catch {}
|
|
34
|
-
return
|
|
34
|
+
return "#";
|
|
35
35
|
}, [href]);
|
|
36
36
|
|
|
37
37
|
return <a href={safeHref}>{children}</a>;
|
|
@@ -55,17 +55,15 @@ function SafeLink({ href, children }) {
|
|
|
55
55
|
|
|
56
56
|
```jsx
|
|
57
57
|
// DANGEROUS - injecting user data into SSR without escaping
|
|
58
|
-
<script>
|
|
59
|
-
window.__INITIAL_STATE__ = {JSON.stringify(userControlledData)}
|
|
60
|
-
</script>
|
|
58
|
+
<script>window.__INITIAL_STATE__ = {JSON.stringify(userControlledData)}</script>;
|
|
61
59
|
|
|
62
60
|
// SAFE - serialize with escaping
|
|
63
|
-
import serialize from
|
|
61
|
+
import serialize from "serialize-javascript";
|
|
64
62
|
<script
|
|
65
63
|
dangerouslySetInnerHTML={{
|
|
66
|
-
__html: `window.__INITIAL_STATE__ = ${serialize(data, { isJSON: true })}
|
|
64
|
+
__html: `window.__INITIAL_STATE__ = ${serialize(data, { isJSON: true })}`,
|
|
67
65
|
}}
|
|
68
|
-
|
|
66
|
+
/>;
|
|
69
67
|
```
|
|
70
68
|
|
|
71
69
|
## Astro Security
|
|
@@ -116,20 +114,20 @@ const Component = await loadComponent();
|
|
|
116
114
|
// src/pages/api/data.js
|
|
117
115
|
export async function POST({ request }) {
|
|
118
116
|
// Validate Content-Type
|
|
119
|
-
const contentType = request.headers.get(
|
|
120
|
-
if (!contentType?.includes(
|
|
121
|
-
return new Response(
|
|
117
|
+
const contentType = request.headers.get("content-type");
|
|
118
|
+
if (!contentType?.includes("application/json")) {
|
|
119
|
+
return new Response("Invalid content type", { status: 415 });
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
// Validate and sanitize input
|
|
125
123
|
const body = await request.json();
|
|
126
124
|
if (!validateInput(body)) {
|
|
127
|
-
return new Response(
|
|
125
|
+
return new Response("Invalid input", { status: 400 });
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
// Process request
|
|
131
129
|
return new Response(JSON.stringify(result), {
|
|
132
|
-
headers: {
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
133
131
|
});
|
|
134
132
|
}
|
|
135
133
|
```
|
|
@@ -177,12 +175,12 @@ export async function POST({ request }) {
|
|
|
177
175
|
twig:
|
|
178
176
|
sandbox:
|
|
179
177
|
policy:
|
|
180
|
-
tags: [
|
|
181
|
-
filters: [
|
|
178
|
+
tags: ["if", "for", "set"]
|
|
179
|
+
filters: ["escape", "upper", "lower"]
|
|
182
180
|
methods:
|
|
183
|
-
Symfony\Component\Routing\Generator\UrlGeneratorInterface: [
|
|
181
|
+
Symfony\Component\Routing\Generator\UrlGeneratorInterface: ["generate"]
|
|
184
182
|
properties: []
|
|
185
|
-
functions: [
|
|
183
|
+
functions: ["path", "url"]
|
|
186
184
|
```
|
|
187
185
|
|
|
188
186
|
### CSRF in Forms
|
|
@@ -207,31 +205,35 @@ Bun.serve({
|
|
|
207
205
|
const url = new URL(req.url);
|
|
208
206
|
|
|
209
207
|
// Validate origin for CORS
|
|
210
|
-
const origin = req.headers.get(
|
|
208
|
+
const origin = req.headers.get("origin");
|
|
211
209
|
if (origin && !isAllowedOrigin(origin)) {
|
|
212
|
-
return new Response(
|
|
210
|
+
return new Response("Forbidden", { status: 403 });
|
|
213
211
|
}
|
|
214
212
|
|
|
215
213
|
// Rate limiting
|
|
216
214
|
if (isRateLimited(req)) {
|
|
217
|
-
return new Response(
|
|
215
|
+
return new Response("Too Many Requests", { status: 429 });
|
|
218
216
|
}
|
|
219
217
|
|
|
220
218
|
return handleRequest(req);
|
|
221
|
-
}
|
|
219
|
+
},
|
|
222
220
|
});
|
|
223
221
|
```
|
|
224
222
|
|
|
225
223
|
### File Handling
|
|
226
224
|
|
|
227
225
|
```javascript
|
|
226
|
+
const path = require("path");
|
|
227
|
+
|
|
228
228
|
// Validate file paths
|
|
229
229
|
function safeReadFile(userPath) {
|
|
230
|
-
const baseDir =
|
|
231
|
-
const
|
|
230
|
+
const baseDir = "/app/public";
|
|
231
|
+
const safeBaseDir = path.resolve(baseDir);
|
|
232
|
+
const resolved = path.resolve(safeBaseDir, userPath);
|
|
233
|
+
const relativePath = path.relative(safeBaseDir, resolved);
|
|
232
234
|
|
|
233
|
-
if (
|
|
234
|
-
throw new Error(
|
|
235
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
236
|
+
throw new Error("Path traversal detected");
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
return Bun.file(resolved).text();
|
|
@@ -244,34 +246,34 @@ function safeReadFile(userPath) {
|
|
|
244
246
|
|
|
245
247
|
```javascript
|
|
246
248
|
// NEVER store sensitive data in localStorage
|
|
247
|
-
localStorage.setItem(
|
|
249
|
+
localStorage.setItem("token", jwt); // DANGEROUS
|
|
248
250
|
|
|
249
251
|
// Use httpOnly cookies for tokens instead
|
|
250
252
|
// Or store in memory with short expiration
|
|
251
253
|
|
|
252
254
|
// If localStorage is necessary, encrypt
|
|
253
|
-
import { encrypt, decrypt } from
|
|
254
|
-
localStorage.setItem(
|
|
255
|
+
import { encrypt, decrypt } from "./crypto";
|
|
256
|
+
localStorage.setItem("data", encrypt(sensitiveData, key));
|
|
255
257
|
```
|
|
256
258
|
|
|
257
259
|
### postMessage
|
|
258
260
|
|
|
259
261
|
```javascript
|
|
260
262
|
// Always validate origin and data
|
|
261
|
-
window.addEventListener(
|
|
263
|
+
window.addEventListener("message", (event) => {
|
|
262
264
|
// Validate origin
|
|
263
|
-
const allowedOrigins = [
|
|
265
|
+
const allowedOrigins = ["https://trusted.com"];
|
|
264
266
|
if (!allowedOrigins.includes(event.origin)) return;
|
|
265
267
|
|
|
266
268
|
// Validate data structure
|
|
267
|
-
if (typeof event.data !==
|
|
268
|
-
if (![
|
|
269
|
+
if (typeof event.data !== "object") return;
|
|
270
|
+
if (!["action1", "action2"].includes(event.data.type)) return;
|
|
269
271
|
|
|
270
272
|
handleMessage(event.data);
|
|
271
273
|
});
|
|
272
274
|
|
|
273
275
|
// Always specify target origin when sending
|
|
274
|
-
iframe.contentWindow.postMessage(data,
|
|
276
|
+
iframe.contentWindow.postMessage(data, "https://specific-origin.com");
|
|
275
277
|
// NEVER use '*' for sensitive data
|
|
276
278
|
```
|
|
277
279
|
|
|
@@ -282,23 +284,23 @@ iframe.contentWindow.postMessage(data, 'https://specific-origin.com');
|
|
|
282
284
|
const wss = new WebSocket.Server({
|
|
283
285
|
server,
|
|
284
286
|
verifyClient: ({ origin, req }, callback) => {
|
|
285
|
-
const allowed = [
|
|
287
|
+
const allowed = ["https://myapp.com"];
|
|
286
288
|
callback(allowed.includes(origin));
|
|
287
|
-
}
|
|
289
|
+
},
|
|
288
290
|
});
|
|
289
291
|
|
|
290
292
|
// Validate messages
|
|
291
|
-
wss.on(
|
|
292
|
-
ws.on(
|
|
293
|
+
wss.on("connection", (ws) => {
|
|
294
|
+
ws.on("message", (data) => {
|
|
293
295
|
try {
|
|
294
296
|
const msg = JSON.parse(data);
|
|
295
297
|
if (!isValidMessage(msg)) {
|
|
296
|
-
ws.close(1008,
|
|
298
|
+
ws.close(1008, "Invalid message");
|
|
297
299
|
return;
|
|
298
300
|
}
|
|
299
301
|
handleMessage(msg);
|
|
300
302
|
} catch {
|
|
301
|
-
ws.close(1008,
|
|
303
|
+
ws.close(1008, "Invalid JSON");
|
|
302
304
|
}
|
|
303
305
|
});
|
|
304
306
|
});
|