@schalkneethling/toolkit 0.2.0 → 0.4.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.
Files changed (31) hide show
  1. package/README.md +29 -7
  2. package/commands/rpm-advance.md +13 -0
  3. package/commands/rpm-checkpoint.md +13 -0
  4. package/commands/rpm-feedback.md +15 -0
  5. package/commands/rpm-handoff.md +14 -0
  6. package/commands/rpm-review.md +13 -0
  7. package/commands/rpm-start.md +13 -0
  8. package/dist/index.mjs +90 -6
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +2 -1
  11. package/skills/css-coder/SKILL.md +95 -0
  12. package/skills/css-coder/references/patterns.md +224 -0
  13. package/skills/css-tokens/README.md +152 -0
  14. package/skills/css-tokens/SKILL.md +125 -0
  15. package/skills/css-tokens/references/tokens.css +162 -0
  16. package/skills/frontend-security/SKILL.md +134 -0
  17. package/skills/frontend-security/references/csp-configuration.md +191 -0
  18. package/skills/frontend-security/references/csrf-protection.md +327 -0
  19. package/skills/frontend-security/references/dom-security.md +229 -0
  20. package/skills/frontend-security/references/file-upload-security.md +310 -0
  21. package/skills/frontend-security/references/framework-patterns.md +307 -0
  22. package/skills/frontend-security/references/input-validation.md +232 -0
  23. package/skills/frontend-security/references/jwt-security.md +300 -0
  24. package/skills/frontend-security/references/nodejs-npm-security.md +261 -0
  25. package/skills/frontend-security/references/xss-prevention.md +163 -0
  26. package/skills/frontend-testing/SKILL.md +357 -0
  27. package/skills/frontend-testing/references/accessibility-testing.md +368 -0
  28. package/skills/frontend-testing/references/aria-snapshots.md +517 -0
  29. package/skills/frontend-testing/references/locator-strategies.md +295 -0
  30. package/skills/frontend-testing/references/visual-regression.md +466 -0
  31. 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