xploitscan-shared-rules 1.1.0 → 1.2.1

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.cjs CHANGED
@@ -472,26 +472,42 @@ var sqlInjection = {
472
472
  description: "String concatenation or template literals in SQL queries allow attackers to execute arbitrary database commands.",
473
473
  check(content, filePath) {
474
474
  const patterns = [
475
- // Template literals in SQL
476
- /(?:query|execute|raw|sql)\s*\(\s*`[^`]*\$\{/gi,
477
- // String concatenation in SQL
478
- /(?:query|execute)\s*\(\s*["'][^"']*["']\s*\+/gi,
479
- // Direct variable interpolation
475
+ // Template literal passed as first arg to query/execute/raw/sql/
476
+ // queryRaw/queryRawUnsafe/execute. Examples that SHOULD fire:
477
+ // db.query(`SELECT ... ${x}`)
478
+ // prisma.$queryRawUnsafe(`... ${x}`)
479
+ // knex.raw(`... ${x}`)
480
+ /(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*`[^`]*\$\{/gi,
481
+ // String concatenation in SQL — now includes raw() and sql() and
482
+ // Drizzle's sql.raw() / Prisma's $queryRawUnsafe() variants. Previous
483
+ // version only covered query()/execute() and missed knex.raw("..." + x).
484
+ //
485
+ // The string literal part uses a proper "quoted string" regex that
486
+ // allows the opposite quote type inside (common in SQL — "WHERE x = 'a'").
487
+ // The older `[^"']*` class bailed out at the first inner quote and
488
+ // missed every realistic SQL-containing concat.
489
+ /(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
490
+ // Direct variable interpolation in a SQL verb context
480
491
  /(?:SELECT|INSERT|UPDATE|DELETE|WHERE)\s+.*\$\{(?!.*parameterized)/gi
481
492
  ];
482
493
  const matches = [];
483
494
  const usesParams = /\?\s*,|\$\d+|:[\w]+|\bprepare\b|\bplaceholder\b/i.test(content);
484
495
  if (usesParams) return [];
485
496
  for (const pattern of patterns) {
486
- matches.push(
487
- ...findMatches(
488
- content,
489
- pattern,
490
- sqlInjection,
491
- filePath,
492
- () => "Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])"
493
- )
497
+ const raw = findMatches(
498
+ content,
499
+ pattern,
500
+ sqlInjection,
501
+ filePath,
502
+ () => "Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])"
494
503
  );
504
+ for (const m of raw) {
505
+ const lineText = content.split("\n")[m.line - 1] || "";
506
+ if (/\bPrisma\.sql\s*`/.test(lineText)) continue;
507
+ if (/\bsql\s*`/.test(lineText) && !/\bsql\.raw\s*\(/.test(lineText)) continue;
508
+ if (/\$queryRaw\s*\(\s*Prisma\.sql|\$executeRaw\s*\(\s*Prisma\.sql/.test(lineText)) continue;
509
+ matches.push(m);
510
+ }
495
511
  }
496
512
  return matches;
497
513
  }
@@ -2617,7 +2633,7 @@ function calculateGrade(findings, _totalFiles) {
2617
2633
  else if (medium >= 1) grade = capGrade("A");
2618
2634
  let summary;
2619
2635
  if (critical > 0) {
2620
- summary = `${critical} critical ${critical === 1 ? "vulnerability" : "vulnerabilities"} require immediate attention.`;
2636
+ summary = `${critical} critical ${critical === 1 ? "vulnerability requires" : "vulnerabilities require"} immediate attention.`;
2621
2637
  } else if (high >= 3) {
2622
2638
  summary = `${high} high-severity issues require urgent attention.`;
2623
2639
  } else if (high > 0) {
@@ -3032,13 +3048,22 @@ var commandInjection = {
3032
3048
  // Node.js — require standalone exec/execSync, not db.exec() or conn.exec()
3033
3049
  /(?<![.\w])(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
3034
3050
  /child_process.*exec\s*\(\s*(?!["'`])/g,
3051
+ // spawn / execFile / exec with shell: true AND a template literal
3052
+ // first arg. `shell: true` converts the first argument into a string
3053
+ // passed to `/bin/sh -c`, so any ${} interpolation is a shell injection
3054
+ // opportunity. Without shell: true, spawn/execFile are safe because
3055
+ // the command and args are kept separate. Previously this class of
3056
+ // bug was missed — the "hasSafe" skip below assumed any spawn in the
3057
+ // file was fine, which is the opposite of true with shell: true.
3058
+ /(?<![.\w])(?:spawn|spawnSync|execFile|execFileSync|exec|execSync)\s*\(\s*`[^`]*\$\{[\s\S]*?shell\s*:\s*(?:true|["'][^"']+["'])/gi,
3035
3059
  // Python
3036
3060
  /os\.system\s*\(\s*(?!["'`].*["'`]\s*\))/g,
3037
3061
  /subprocess\.(?:call|run|Popen)\s*\([^)]*shell\s*=\s*True/gi,
3038
3062
  // Ruby
3039
3063
  /system\s*\(\s*["'].*#\{/g
3040
3064
  ];
3041
- const hasSafe = /execFile|spawn|escapeshellarg|shlex\.quote|shellEscape/i.test(content);
3065
+ const hasShellTrue = /shell\s*:\s*(?:true|["'][^"']+["'])/i.test(content);
3066
+ const hasSafe = !hasShellTrue && /execFile|spawn|escapeshellarg|shlex\.quote|shellEscape/i.test(content);
3042
3067
  if (hasSafe) return [];
3043
3068
  for (const p of patterns) {
3044
3069
  const raw = findMatches(
@@ -4539,7 +4564,7 @@ var hardcodedSupabaseServiceRole = {
4539
4564
  if (isTestFile(filePath)) return [];
4540
4565
  if (!SECRET_FILE_EXT.test(filePath)) return [];
4541
4566
  if (LOCK_FILE_RE.test(filePath)) return [];
4542
- const pattern = /(?:service[_-]?role[_-]?key|SUPABASE_SERVICE_ROLE_KEY|supabaseServiceRole)\s*[:=]\s*["'`](eyJ[A-Za-z0-9_\-]{50,})["'`]/gi;
4567
+ const pattern = /(?:service[_-]?role[_-]?key|SUPABASE_SERVICE_ROLE_KEY|supabaseServiceRole)\s*[:=]\s*["'`](eyJ[A-Za-z0-9_\-.]{50,})["'`]/gi;
4543
4568
  const findings = [];
4544
4569
  let m;
4545
4570
  while ((m = pattern.exec(content)) !== null) {