@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,229 @@
|
|
|
1
|
+
# DOM Security Reference
|
|
2
|
+
|
|
3
|
+
## DOM-Based XSS Prevention
|
|
4
|
+
|
|
5
|
+
### Rule #1: HTML Subcontext
|
|
6
|
+
|
|
7
|
+
Untrusted data in HTML element content:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
// UNSAFE
|
|
11
|
+
element.innerHTML = '<div>' + userInput + '</div>';
|
|
12
|
+
|
|
13
|
+
// SAFE - use textContent
|
|
14
|
+
element.textContent = userInput;
|
|
15
|
+
|
|
16
|
+
// SAFE - create elements programmatically
|
|
17
|
+
const div = document.createElement('div');
|
|
18
|
+
div.textContent = userInput;
|
|
19
|
+
parent.appendChild(div);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Rule #2: JavaScript Context
|
|
23
|
+
|
|
24
|
+
Untrusted data in JavaScript:
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
// UNSAFE - string concatenation in script
|
|
28
|
+
const script = 'var x = "' + userInput + '"';
|
|
29
|
+
|
|
30
|
+
// UNSAFE - dynamic property access
|
|
31
|
+
window[userInput]();
|
|
32
|
+
|
|
33
|
+
// SAFE - use data attributes
|
|
34
|
+
element.dataset.value = userInput;
|
|
35
|
+
const value = element.dataset.value;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Rule #3: HTML Attribute Context
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// UNSAFE - event handlers
|
|
42
|
+
element.setAttribute('onclick', userInput);
|
|
43
|
+
|
|
44
|
+
// UNSAFE - dangerous attributes
|
|
45
|
+
element.setAttribute('href', userInput); // javascript: URLs
|
|
46
|
+
element.setAttribute('src', userInput); // script injection
|
|
47
|
+
|
|
48
|
+
// SAFE - safe attributes only
|
|
49
|
+
const safeAttributes = ['title', 'alt', 'class', 'id', 'name'];
|
|
50
|
+
if (safeAttributes.includes(attributeName)) {
|
|
51
|
+
element.setAttribute(attributeName, userInput);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Rule #4: CSS Context
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// UNSAFE - expression injection
|
|
59
|
+
element.style.cssText = userInput;
|
|
60
|
+
element.setAttribute('style', userInput);
|
|
61
|
+
|
|
62
|
+
// SAFE - set specific properties
|
|
63
|
+
element.style.backgroundColor = sanitizeColor(userInput);
|
|
64
|
+
|
|
65
|
+
function sanitizeColor(input) {
|
|
66
|
+
// Only allow safe color values
|
|
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 : 'inherit';
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Rule #5: URL Context
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
// UNSAFE - unvalidated URLs
|
|
76
|
+
location.href = userInput;
|
|
77
|
+
window.open(userInput);
|
|
78
|
+
element.setAttribute('href', userInput);
|
|
79
|
+
|
|
80
|
+
// SAFE - validate URL protocol
|
|
81
|
+
function validateUrl(input) {
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(input, window.location.origin);
|
|
84
|
+
const allowedProtocols = ['http:', 'https:', 'mailto:'];
|
|
85
|
+
if (!allowedProtocols.includes(url.protocol)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return url.href;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const safeUrl = validateUrl(userInput);
|
|
95
|
+
if (safeUrl) {
|
|
96
|
+
location.href = safeUrl;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## DOM Clobbering Prevention
|
|
101
|
+
|
|
102
|
+
### What is DOM Clobbering?
|
|
103
|
+
|
|
104
|
+
Named elements (id, name attributes) create global variables:
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<!-- This creates window.config -->
|
|
108
|
+
<img id="config" src="x">
|
|
109
|
+
|
|
110
|
+
<!-- JavaScript that assumes config is an object will break -->
|
|
111
|
+
<script>
|
|
112
|
+
console.log(config.apiKey); // Error: HTMLImageElement has no apiKey
|
|
113
|
+
</script>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Prevention Strategies
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
// 1. Use Object.hasOwn() or hasOwnProperty()
|
|
120
|
+
if (Object.hasOwn(window, 'config') && typeof config === 'object') {
|
|
121
|
+
// Safe to use config
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Access through document methods
|
|
125
|
+
const element = document.getElementById('config');
|
|
126
|
+
|
|
127
|
+
// 3. Use Map instead of objects for user-controlled keys
|
|
128
|
+
const userConfig = new Map();
|
|
129
|
+
userConfig.set(userKey, userValue);
|
|
130
|
+
|
|
131
|
+
// 4. Freeze global objects
|
|
132
|
+
Object.freeze(window.config);
|
|
133
|
+
|
|
134
|
+
// 5. Use nullish coalescing with type checking
|
|
135
|
+
const config = window.config ?? {};
|
|
136
|
+
if (typeof config.apiKey === 'string') {
|
|
137
|
+
// Safe to use
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### HTML Sanitization Against Clobbering
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// DOMPurify with clobbering protection
|
|
145
|
+
const clean = DOMPurify.sanitize(dirty, {
|
|
146
|
+
SANITIZE_DOM: true, // Remove clobbering vectors
|
|
147
|
+
SANITIZE_NAMED_PROPS: true
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Secure DOM APIs
|
|
152
|
+
|
|
153
|
+
### Safe Methods
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// Text content (no HTML parsing)
|
|
157
|
+
element.textContent = userInput;
|
|
158
|
+
document.createTextNode(userInput);
|
|
159
|
+
|
|
160
|
+
// Attribute manipulation (for safe attributes)
|
|
161
|
+
element.setAttribute('data-value', userInput);
|
|
162
|
+
element.classList.add(sanitizedClass);
|
|
163
|
+
|
|
164
|
+
// Query selectors (read-only)
|
|
165
|
+
document.querySelector(selector);
|
|
166
|
+
document.querySelectorAll(selector);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Dangerous Methods (Require Sanitization)
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
// HTML parsing methods
|
|
173
|
+
element.innerHTML = sanitized;
|
|
174
|
+
element.outerHTML = sanitized;
|
|
175
|
+
element.insertAdjacentHTML(position, sanitized);
|
|
176
|
+
document.write(sanitized); // Avoid entirely
|
|
177
|
+
|
|
178
|
+
// Script execution
|
|
179
|
+
eval(); // Never use with user input
|
|
180
|
+
new Function(); // Never use with user input
|
|
181
|
+
setTimeout(string); // Never pass strings
|
|
182
|
+
setInterval(string); // Never pass strings
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## postMessage Security
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
// Sender - specify exact origin
|
|
189
|
+
targetWindow.postMessage(data, 'https://trusted-domain.com');
|
|
190
|
+
|
|
191
|
+
// Receiver - always validate origin
|
|
192
|
+
window.addEventListener('message', (event) => {
|
|
193
|
+
// Validate origin
|
|
194
|
+
if (event.origin !== 'https://trusted-domain.com') {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate data structure
|
|
199
|
+
if (typeof event.data !== 'object' || !event.data.type) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Process trusted message
|
|
204
|
+
handleMessage(event.data);
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Trusted Types (Modern Browsers)
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
// Enable Trusted Types via CSP
|
|
212
|
+
// Content-Security-Policy: require-trusted-types-for 'script'
|
|
213
|
+
|
|
214
|
+
// Create a policy
|
|
215
|
+
const policy = trustedTypes.createPolicy('default', {
|
|
216
|
+
createHTML: (input) => DOMPurify.sanitize(input),
|
|
217
|
+
createScript: () => { throw new Error('Scripts not allowed'); },
|
|
218
|
+
createScriptURL: (input) => {
|
|
219
|
+
const url = new URL(input, location.origin);
|
|
220
|
+
if (url.origin === location.origin) return input;
|
|
221
|
+
throw new Error('Invalid script URL');
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Usage
|
|
226
|
+
element.innerHTML = policy.createHTML(userInput);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# File Upload Security Reference
|
|
2
|
+
|
|
3
|
+
## Core Protection Checklist
|
|
4
|
+
|
|
5
|
+
- [ ] Validate file extension (allowlist only)
|
|
6
|
+
- [ ] Validate Content-Type header
|
|
7
|
+
- [ ] Validate file signature (magic bytes)
|
|
8
|
+
- [ ] Generate new random filename
|
|
9
|
+
- [ ] Enforce file size limits
|
|
10
|
+
- [ ] Store outside webroot
|
|
11
|
+
- [ ] Scan for malware
|
|
12
|
+
- [ ] Require authentication
|
|
13
|
+
- [ ] Implement CSRF protection
|
|
14
|
+
|
|
15
|
+
## Extension Validation
|
|
16
|
+
|
|
17
|
+
### Allowlist Approach
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const ALLOWED_EXTENSIONS = {
|
|
21
|
+
images: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
|
|
22
|
+
documents: ['.pdf', '.docx', '.xlsx'],
|
|
23
|
+
data: ['.csv', '.json']
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function validateExtension(filename, category) {
|
|
27
|
+
const ext = path.extname(filename).toLowerCase();
|
|
28
|
+
return ALLOWED_EXTENSIONS[category]?.includes(ext) ?? false;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Dangerous Extensions to Block
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
const DANGEROUS_EXTENSIONS = [
|
|
36
|
+
// Server-side execution
|
|
37
|
+
'.php', '.php3', '.php4', '.php5', '.phtml',
|
|
38
|
+
'.asp', '.aspx', '.ascx', '.ashx',
|
|
39
|
+
'.jsp', '.jspx', '.jspa',
|
|
40
|
+
'.cgi', '.pl', '.py', '.rb',
|
|
41
|
+
|
|
42
|
+
// Windows executable
|
|
43
|
+
'.exe', '.dll', '.bat', '.cmd', '.com', '.msi', '.ps1',
|
|
44
|
+
|
|
45
|
+
// Script files
|
|
46
|
+
'.js', '.vbs', '.wsf', '.hta',
|
|
47
|
+
|
|
48
|
+
// Config files
|
|
49
|
+
'.htaccess', '.htpasswd', '.config', '.ini',
|
|
50
|
+
|
|
51
|
+
// Archive (can contain malicious files)
|
|
52
|
+
'.zip', '.tar', '.gz', '.rar', '.7z'
|
|
53
|
+
];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Double Extension Prevention
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
function sanitizeFilename(filename) {
|
|
60
|
+
// Remove all extensions except the last
|
|
61
|
+
const parts = filename.split('.');
|
|
62
|
+
if (parts.length > 2) {
|
|
63
|
+
return `${parts[0]}.${parts[parts.length - 1]}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Or generate completely new filename
|
|
67
|
+
const ext = path.extname(filename).toLowerCase();
|
|
68
|
+
const uuid = crypto.randomUUID();
|
|
69
|
+
return `${uuid}${ext}`;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Content-Type Validation
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
const ALLOWED_MIME_TYPES = {
|
|
77
|
+
'.jpg': ['image/jpeg'],
|
|
78
|
+
'.jpeg': ['image/jpeg'],
|
|
79
|
+
'.png': ['image/png'],
|
|
80
|
+
'.gif': ['image/gif'],
|
|
81
|
+
'.webp': ['image/webp'],
|
|
82
|
+
'.pdf': ['application/pdf'],
|
|
83
|
+
'.docx': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document']
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function validateMimeType(file) {
|
|
87
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
88
|
+
const allowedMimes = ALLOWED_MIME_TYPES[ext];
|
|
89
|
+
|
|
90
|
+
if (!allowedMimes) return false;
|
|
91
|
+
return allowedMimes.includes(file.mimetype);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## File Signature (Magic Bytes) Validation
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const FILE_SIGNATURES = {
|
|
99
|
+
jpg: Buffer.from([0xFF, 0xD8, 0xFF]),
|
|
100
|
+
png: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
|
|
101
|
+
gif: Buffer.from([0x47, 0x49, 0x46, 0x38]),
|
|
102
|
+
pdf: Buffer.from([0x25, 0x50, 0x44, 0x46]),
|
|
103
|
+
zip: Buffer.from([0x50, 0x4B, 0x03, 0x04])
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
async function validateFileSignature(filePath, expectedType) {
|
|
107
|
+
const buffer = Buffer.alloc(8);
|
|
108
|
+
const fd = await fs.open(filePath, 'r');
|
|
109
|
+
await fd.read(buffer, 0, 8, 0);
|
|
110
|
+
await fd.close();
|
|
111
|
+
|
|
112
|
+
const signature = FILE_SIGNATURES[expectedType];
|
|
113
|
+
if (!signature) return false;
|
|
114
|
+
|
|
115
|
+
return buffer.slice(0, signature.length).equals(signature);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Safe Storage
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
const multer = require('multer');
|
|
123
|
+
const path = require('path');
|
|
124
|
+
const crypto = require('crypto');
|
|
125
|
+
|
|
126
|
+
// Store OUTSIDE webroot
|
|
127
|
+
const UPLOAD_DIR = '/var/app/uploads'; // Not in /public/
|
|
128
|
+
|
|
129
|
+
const storage = multer.diskStorage({
|
|
130
|
+
destination: (req, file, cb) => {
|
|
131
|
+
// Organize by date
|
|
132
|
+
const date = new Date().toISOString().split('T')[0];
|
|
133
|
+
const dir = path.join(UPLOAD_DIR, date);
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
cb(null, dir);
|
|
136
|
+
},
|
|
137
|
+
filename: (req, file, cb) => {
|
|
138
|
+
// Generate random filename
|
|
139
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
140
|
+
const name = crypto.randomBytes(16).toString('hex');
|
|
141
|
+
cb(null, `${name}${ext}`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const upload = multer({
|
|
146
|
+
storage,
|
|
147
|
+
limits: {
|
|
148
|
+
fileSize: 5 * 1024 * 1024, // 5MB
|
|
149
|
+
files: 1
|
|
150
|
+
},
|
|
151
|
+
fileFilter: (req, file, cb) => {
|
|
152
|
+
if (!validateMimeType(file)) {
|
|
153
|
+
cb(new Error('Invalid file type'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
cb(null, true);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Secure File Serving
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// Serve files through application, not directly
|
|
165
|
+
app.get('/files/:id', async (req, res) => {
|
|
166
|
+
// Verify user authorization
|
|
167
|
+
if (!req.user || !canAccessFile(req.user, req.params.id)) {
|
|
168
|
+
return res.status(403).send('Forbidden');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get file from database (not from user input)
|
|
172
|
+
const fileRecord = await db.getFile(req.params.id);
|
|
173
|
+
if (!fileRecord) return res.status(404).send('Not found');
|
|
174
|
+
|
|
175
|
+
// Set safe headers
|
|
176
|
+
res.setHeader('Content-Type', fileRecord.mimeType);
|
|
177
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileRecord.safeName}"`);
|
|
178
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
179
|
+
|
|
180
|
+
// Stream file
|
|
181
|
+
const stream = fs.createReadStream(fileRecord.path);
|
|
182
|
+
stream.pipe(res);
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Image Rewriting
|
|
187
|
+
|
|
188
|
+
Destroy potential malicious content by re-encoding images:
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
const sharp = require('sharp');
|
|
192
|
+
|
|
193
|
+
async function sanitizeImage(inputPath, outputPath) {
|
|
194
|
+
await sharp(inputPath)
|
|
195
|
+
.rotate() // Apply EXIF orientation
|
|
196
|
+
.toFormat('jpeg', { quality: 90 }) // Re-encode
|
|
197
|
+
.toFile(outputPath);
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## ZIP File Handling
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
const AdmZip = require('adm-zip');
|
|
205
|
+
const path = require('path');
|
|
206
|
+
|
|
207
|
+
function safeExtractZip(zipPath, destDir, maxSize = 100 * 1024 * 1024) {
|
|
208
|
+
const zip = new AdmZip(zipPath);
|
|
209
|
+
const entries = zip.getEntries();
|
|
210
|
+
|
|
211
|
+
let totalSize = 0;
|
|
212
|
+
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
// Check for path traversal
|
|
215
|
+
const resolvedDest = path.resolve(destDir);
|
|
216
|
+
const resolvedEntry = path.resolve(destDir, entry.entryName);
|
|
217
|
+
const relativeEntry = path.relative(resolvedDest, resolvedEntry);
|
|
218
|
+
|
|
219
|
+
if (relativeEntry.startsWith('..') || path.isAbsolute(relativeEntry)) {
|
|
220
|
+
throw new Error('Path traversal detected');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for zip bomb
|
|
224
|
+
totalSize += entry.header.size;
|
|
225
|
+
if (totalSize > maxSize) {
|
|
226
|
+
throw new Error('Extracted size exceeds limit');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check compression ratio (zip bomb indicator)
|
|
230
|
+
const ratio = entry.header.size / entry.header.compressedSize;
|
|
231
|
+
if (ratio > 100) {
|
|
232
|
+
throw new Error('Suspicious compression ratio');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
zip.extractAllTo(destDir, true);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Express.js Complete Example
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
const express = require('express');
|
|
244
|
+
const multer = require('multer');
|
|
245
|
+
const path = require('path');
|
|
246
|
+
const crypto = require('crypto');
|
|
247
|
+
const fs = require('fs').promises;
|
|
248
|
+
|
|
249
|
+
const app = express();
|
|
250
|
+
|
|
251
|
+
// Configuration
|
|
252
|
+
const UPLOAD_DIR = '/var/app/uploads';
|
|
253
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
254
|
+
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
|
|
255
|
+
|
|
256
|
+
// Multer setup
|
|
257
|
+
const upload = multer({
|
|
258
|
+
storage: multer.memoryStorage(),
|
|
259
|
+
limits: { fileSize: MAX_FILE_SIZE },
|
|
260
|
+
fileFilter: (req, file, cb) => {
|
|
261
|
+
if (!ALLOWED_TYPES.includes(file.mimetype)) {
|
|
262
|
+
cb(new multer.MulterError('LIMIT_UNEXPECTED_FILE'));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
cb(null, true);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Upload endpoint
|
|
270
|
+
app.post('/upload',
|
|
271
|
+
requireAuth, // Authentication
|
|
272
|
+
verifyToken, // CSRF token
|
|
273
|
+
upload.single('file'), // File handling
|
|
274
|
+
async (req, res) => {
|
|
275
|
+
try {
|
|
276
|
+
const file = req.file;
|
|
277
|
+
if (!file) return res.status(400).json({ error: 'No file' });
|
|
278
|
+
|
|
279
|
+
// Validate magic bytes
|
|
280
|
+
if (!validateMagicBytes(file.buffer, file.mimetype)) {
|
|
281
|
+
return res.status(400).json({ error: 'Invalid file' });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Generate safe filename
|
|
285
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
286
|
+
const filename = `${crypto.randomUUID()}${ext}`;
|
|
287
|
+
const filepath = path.join(UPLOAD_DIR, filename);
|
|
288
|
+
|
|
289
|
+
// Save file
|
|
290
|
+
await fs.writeFile(filepath, file.buffer);
|
|
291
|
+
|
|
292
|
+
// Store metadata in database
|
|
293
|
+
const fileRecord = await db.createFile({
|
|
294
|
+
userId: req.user.id,
|
|
295
|
+
filename,
|
|
296
|
+
originalName: file.originalname,
|
|
297
|
+
mimeType: file.mimetype,
|
|
298
|
+
size: file.size
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
res.json({ id: fileRecord.id });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(error);
|
|
304
|
+
res.status(500).json({ error: 'Upload failed' });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
|