@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.
@@ -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
- /localStorage\.setItem\s*\(\s*['"`](?:token|jwt|auth|session|api[_-]?key|secret|password|credential)/i,
130
- /sessionStorage\.setItem\s*\(\s*['"`](?:token|jwt|auth|session|api[_-]?key|secret|password|credential)/i,
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
- /\.from\s*\(\s*['"`][^'"`]+['"`]\s*\)\.(?:select|insert|update|delete)/,
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: 'Merging user input into objects can allow prototype pollution attacks',
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
- /Object\.assign\s*\(\s*\{\}\s*,[^)]*(?:req\.|body\.|params\.|query\.)/,
413
- /\{\s*\.\.\.(?:req|body|params|query)/,
414
- /lodash\.merge\s*\([^)]*(?:req\.|body\.)/,
415
- /deepmerge\s*\([^)]*(?:req\.|body\.)/,
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
  // ============================================