@indicated/vibeguard 1.3.2 → 1.5.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/cli/commands/scan.d.ts.map +1 -1
- package/dist/cli/commands/scan.js +5 -0
- package/dist/cli/commands/scan.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +250 -49
- package/dist/mcp/server.js.map +1 -1
- package/dist/scanner/parsers/javascript.d.ts.map +1 -1
- package/dist/scanner/parsers/javascript.js +43 -1
- package/dist/scanner/parsers/javascript.js.map +1 -1
- package/dist/scanner/rules/definitions.d.ts.map +1 -1
- package/dist/scanner/rules/definitions.js +26 -9
- package/dist/scanner/rules/definitions.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/scan.ts +6 -0
- package/src/mcp/server.ts +292 -53
- package/src/scanner/parsers/javascript.ts +52 -1
- package/src/scanner/rules/definitions.ts +26 -9
|
@@ -109,13 +109,49 @@ export function scanWithAST(
|
|
|
109
109
|
},
|
|
110
110
|
|
|
111
111
|
'xss-innerhtml': (path: NodePath) => {
|
|
112
|
+
// Check if file imports a sanitizer - if so, assume proper usage
|
|
113
|
+
const codeLC = context.code.toLowerCase();
|
|
114
|
+
const hasSanitizer = codeLC.includes('dompurify') ||
|
|
115
|
+
codeLC.includes('sanitize-html') ||
|
|
116
|
+
codeLC.includes('xss') ||
|
|
117
|
+
codeLC.includes('escapehtml') ||
|
|
118
|
+
codeLC.includes('escape-html') ||
|
|
119
|
+
codeLC.includes('htmlsanitizer') ||
|
|
120
|
+
/function\s+escapehtml/i.test(context.code) ||
|
|
121
|
+
/const\s+escapehtml/i.test(context.code) ||
|
|
122
|
+
/escapehtml\s*[:=]/i.test(context.code);
|
|
123
|
+
|
|
124
|
+
if (hasSanitizer) {
|
|
125
|
+
return null; // File has sanitization, skip innerHTML checks
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
if (path.isAssignmentExpression()) {
|
|
113
129
|
const left = path.node.left;
|
|
130
|
+
const right = path.node.right;
|
|
114
131
|
if (
|
|
115
132
|
t.isMemberExpression(left) &&
|
|
116
133
|
t.isIdentifier(left.property) &&
|
|
117
134
|
left.property.name === 'innerHTML'
|
|
118
135
|
) {
|
|
136
|
+
// Skip if RHS is a string literal (static HTML is safe)
|
|
137
|
+
if (t.isStringLiteral(right)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// Skip if RHS is a template literal with no expressions (static)
|
|
141
|
+
if (t.isTemplateLiteral(right) && right.expressions.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
// Skip if wrapped in sanitizer call
|
|
145
|
+
if (t.isCallExpression(right)) {
|
|
146
|
+
const callCode = context.code.substring(
|
|
147
|
+
right.start || 0,
|
|
148
|
+
right.end || 0
|
|
149
|
+
).toLowerCase();
|
|
150
|
+
if (callCode.includes('sanitize') || callCode.includes('escape') || callCode.includes('purify')) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
const rule = rules.find(r => r.id === 'xss-innerhtml');
|
|
120
156
|
if (rule) {
|
|
121
157
|
const loc = path.node.loc;
|
|
@@ -132,10 +168,25 @@ export function scanWithAST(
|
|
|
132
168
|
}
|
|
133
169
|
}
|
|
134
170
|
|
|
135
|
-
// Check for dangerouslySetInnerHTML in JSX
|
|
171
|
+
// Check for dangerouslySetInnerHTML in JSX - only flag if value is not static
|
|
136
172
|
if (path.isJSXAttribute()) {
|
|
137
173
|
const name = path.node.name;
|
|
138
174
|
if (t.isJSXIdentifier(name) && name.name === 'dangerouslySetInnerHTML') {
|
|
175
|
+
const value = path.node.value;
|
|
176
|
+
// Check if the value is a static string (safe)
|
|
177
|
+
if (t.isJSXExpressionContainer(value) && value.expression) {
|
|
178
|
+
const expr = value.expression;
|
|
179
|
+
// Check if it's an object with __html property that's a string literal
|
|
180
|
+
if (t.isObjectExpression(expr)) {
|
|
181
|
+
const htmlProp = expr.properties.find(
|
|
182
|
+
p => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === '__html'
|
|
183
|
+
);
|
|
184
|
+
if (htmlProp && t.isObjectProperty(htmlProp) && t.isStringLiteral(htmlProp.value)) {
|
|
185
|
+
return null; // Static HTML string is safe
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
139
190
|
const rule = rules.find(r => r.id === 'xss-innerhtml');
|
|
140
191
|
if (rule) {
|
|
141
192
|
const loc = path.node.loc;
|
|
@@ -126,8 +126,9 @@ export const securityRules: SecurityRule[] = [
|
|
|
126
126
|
tier: 'free',
|
|
127
127
|
languages: ['javascript', 'typescript'],
|
|
128
128
|
patterns: [
|
|
129
|
-
|
|
130
|
-
/
|
|
129
|
+
// Only match actual sensitive key names, not prefixes like "sessionStartTime"
|
|
130
|
+
/localStorage\.setItem\s*\(\s*['"`](?:access[_-]?token|refresh[_-]?token|auth[_-]?token|jwt[_-]?token|api[_-]?key|secret[_-]?key|password|private[_-]?key)['"`]/i,
|
|
131
|
+
/sessionStorage\.setItem\s*\(\s*['"`](?:access[_-]?token|refresh[_-]?token|auth[_-]?token|jwt[_-]?token|api[_-]?key|secret[_-]?key|password|private[_-]?key)['"`]/i,
|
|
131
132
|
],
|
|
132
133
|
fix: 'Use httpOnly cookies for sensitive tokens, or encrypt before storage',
|
|
133
134
|
},
|
|
@@ -139,7 +140,20 @@ export const securityRules: SecurityRule[] = [
|
|
|
139
140
|
tier: 'pro',
|
|
140
141
|
languages: ['javascript', 'typescript'],
|
|
141
142
|
patterns: [
|
|
142
|
-
|
|
143
|
+
// Only flag client-side code - server-side using service role is correct pattern
|
|
144
|
+
/createClient\s*\([^)]*\)[\s\S]*\.from\s*\(\s*['"`][^'"`]+['"`]\s*\)\.(?:select|insert|update|delete)/,
|
|
145
|
+
],
|
|
146
|
+
// Exclude server-side API files where service role key usage is correct
|
|
147
|
+
pathExclusions: [
|
|
148
|
+
/\/api\//,
|
|
149
|
+
/\/server\//,
|
|
150
|
+
/\/backend\//,
|
|
151
|
+
/\/routes\//,
|
|
152
|
+
/\/controllers\//,
|
|
153
|
+
/\/services\//,
|
|
154
|
+
/\.server\./,
|
|
155
|
+
/pages\/api\//,
|
|
156
|
+
/app\/api\//,
|
|
143
157
|
],
|
|
144
158
|
astMatcher: 'supabase-no-rls',
|
|
145
159
|
fix: 'Enable Row Level Security on Supabase tables and add policies',
|
|
@@ -404,17 +418,20 @@ export const securityRules: SecurityRule[] = [
|
|
|
404
418
|
{
|
|
405
419
|
id: 'prototype-pollution',
|
|
406
420
|
name: 'Potential Prototype Pollution',
|
|
407
|
-
description: '
|
|
421
|
+
description: 'Deep merging user input can allow prototype pollution attacks',
|
|
408
422
|
severity: 'low',
|
|
409
423
|
tier: 'free',
|
|
410
424
|
languages: ['javascript', 'typescript'],
|
|
411
425
|
patterns: [
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
/lodash\.merge\s*\([^)]*(?:req\.|body\.)/,
|
|
415
|
-
/
|
|
426
|
+
// Only flag actual deep merge operations that can cause prototype pollution
|
|
427
|
+
// Spread operator {...obj} and Object.assign({}, obj) are SAFE - they don't pollute
|
|
428
|
+
/(?:lodash|_)\.merge\s*\([^)]*(?:req\.|body\.|params\.|query\.)/,
|
|
429
|
+
/(?:lodash|_)\.mergeWith\s*\([^)]*(?:req\.|body\.|params\.|query\.)/,
|
|
430
|
+
/(?:lodash|_)\.defaultsDeep\s*\([^)]*(?:req\.|body\.|params\.|query\.)/,
|
|
431
|
+
/deepmerge\s*\([^)]*(?:req\.|body\.|params\.|query\.)/,
|
|
432
|
+
/merge\s*\(\s*\w+\s*,\s*(?:req\.|body\.|params\.|query\.)/,
|
|
416
433
|
],
|
|
417
|
-
fix: 'Validate and sanitize user input before merging. Use Object.create(null) for dictionaries',
|
|
434
|
+
fix: 'Validate and sanitize user input before deep merging. Use Object.create(null) for dictionaries',
|
|
418
435
|
},
|
|
419
436
|
|
|
420
437
|
// ============================================
|