xploitscan-shared-rules 1.7.0 → 1.7.2

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
@@ -1225,8 +1225,16 @@ var sqlInjection = {
1225
1225
  // The older `[^"']*` class bailed out at the first inner quote and
1226
1226
  // missed every realistic SQL-containing concat.
1227
1227
  /(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
1228
- // Direct variable interpolation in a SQL verb context
1229
- /(?:SELECT|INSERT|UPDATE|DELETE|WHERE)\s+.*\$\{(?!.*parameterized)/gi
1228
+ // Direct variable interpolation in a SQL string literal. Requires the
1229
+ // verb to START a quoted/backtick literal AND be followed — within the
1230
+ // same literal — by a SQL structural keyword (FROM/SET/WHERE/…), then a
1231
+ // `${` interpolation. The structural keyword is what distinguishes a
1232
+ // real query from ordinary prose: the previous pattern matched any
1233
+ // SELECT/UPDATE/… word anywhere on a line followed by `${`, so UI copy
1234
+ // like `UPDATE your profile settings ${name}` flagged as a CRITICAL SQL
1235
+ // injection. Real queries ("SELECT … FROM …", "UPDATE … SET …") always
1236
+ // carry a second structural keyword; sentences almost never do.
1237
+ /["'`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^"'`\n]*\b(?:FROM|INTO|SET|VALUES|WHERE|JOIN|RETURNING)\b[^"'`\n]*\$\{(?!.*parameterized)/gi
1230
1238
  ];
1231
1239
  const matches = [];
1232
1240
  const usesParams = /\?\s*,|\?[^?,;]{1,20}\?|\$\d+|:[\w]+|\bprepare\b|\bplaceholders?\b/i.test(content);
@@ -1272,6 +1280,7 @@ var xssVulnerability = {
1272
1280
  // {@html} in Svelte
1273
1281
  /\{@html\s/g
1274
1282
  ];
1283
+ const allLines = content.split("\n");
1275
1284
  const matches = [];
1276
1285
  for (const pattern of patterns) {
1277
1286
  const raw = findMatches(
@@ -1279,12 +1288,18 @@ var xssVulnerability = {
1279
1288
  pattern,
1280
1289
  xssVulnerability,
1281
1290
  filePath,
1282
- () => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)"
1291
+ () => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput). If this site is intentional (JSON-LD structured data, a const boot script, a sandboxed preview), add an inline `// VC007-OK: <reason>` comment above the line to silence."
1283
1292
  );
1284
1293
  for (const m of raw) {
1285
- const lineText = content.split("\n")[m.line - 1] || "";
1294
+ const lineText = allLines[m.line - 1] || "";
1286
1295
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
1287
1296
  if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
1297
+ if (/dangerouslySetInnerHTML/.test(lineText)) {
1298
+ const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
1299
+ if (/JSON\.stringify/i.test(windowText)) continue;
1300
+ const lineStartIdx = allLines.slice(0, m.line - 1).reduce((acc, l) => acc + l.length + 1, 0);
1301
+ if (isInlineSilenced(content, lineStartIdx, "VC007")) continue;
1302
+ }
1288
1303
  matches.push(m);
1289
1304
  }
1290
1305
  }
@@ -1560,23 +1575,35 @@ var unvalidatedRedirect = {
1560
1575
  check(content, filePath) {
1561
1576
  if (isTestFile(filePath)) return [];
1562
1577
  if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];
1563
- const patterns = [
1564
- /window\.location\s*=\s*(?!["'`]https?:\/\/)/g,
1565
- /window\.location\.href\s*=\s*(?!["'`]https?:\/\/)/g,
1566
- /window\.location\.assign\s*\(\s*(?!["'`]https?:\/\/)/g,
1567
- /window\.location\.replace\s*\(\s*(?!["'`]https?:\/\/)/g,
1568
- /res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi
1569
- ];
1570
1578
  const matches = [];
1571
- for (const p of patterns) {
1572
- matches.push(...findMatches(
1573
- content,
1574
- p,
1575
- unvalidatedRedirect,
1576
- filePath,
1577
- () => "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly."
1578
- ));
1579
+ const navPattern = /(?:window\.location(?:\.href)?\s*=\s*|window\.location\.(?:assign|replace)\s*\(\s*)([^;\n)]+)/g;
1580
+ let m;
1581
+ while ((m = navPattern.exec(content)) !== null) {
1582
+ if (isCommentLine(content, m.index)) continue;
1583
+ const value = m[1].trim();
1584
+ const hasInterpolation = /\$\{/.test(value);
1585
+ const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
1586
+ if (!hasInterpolation && isPureLiteral) continue;
1587
+ if (isInlineSilenced(content, m.index, "VC016")) continue;
1588
+ const line = content.substring(0, m.index).split("\n").length;
1589
+ matches.push({
1590
+ rule: "VC016",
1591
+ title: unvalidatedRedirect.title,
1592
+ severity: "high",
1593
+ category: "Injection",
1594
+ file: filePath,
1595
+ line,
1596
+ snippet: getSnippet(content, line),
1597
+ fix: "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly. If the target is a hardcoded/internal path you control, add an inline `// VC016-OK: <reason>` comment to silence."
1598
+ });
1579
1599
  }
1600
+ matches.push(...findMatches(
1601
+ content,
1602
+ /res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi,
1603
+ unvalidatedRedirect,
1604
+ filePath,
1605
+ () => "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly."
1606
+ ));
1580
1607
  return matches;
1581
1608
  }
1582
1609
  };
@@ -8296,11 +8323,20 @@ var SAFE_PATTERNS = [
8296
8323
  /^'sha\d+-/,
8297
8324
  /^nonce-/i,
8298
8325
  // Embedded data URIs
8299
- /^data:[a-z]+\//i
8326
+ /^data:[a-z]+\//i,
8327
+ // Sentry DSN — a publishable, client-side ingest key (it ships to the
8328
+ // browser), not a secret. Shape: https://<hash>@o<orgid>.ingest.<region.>
8329
+ // sentry.io/<projectid>. The generic URL allowlist above deliberately
8330
+ // excludes anything with an `@`, so the DSN would otherwise fire.
8331
+ /^https?:\/\/[0-9a-f]+@o\d+\.ingest\.[a-z0-9.]*sentry\.io\//i,
8332
+ // HTTP security-header directive values (HSTS, etc.) — `max-age=63072000;
8333
+ // includeSubDomains; preload`. Config strings a security scanner actively
8334
+ // tells users to add; never secrets.
8335
+ /^max-age=\d+/i
8300
8336
  ];
8301
8337
  var SKIP_FILES = /\.(css|scss|less|svg|md|txt|html?|xml|yml|yaml|toml|lock|map|woff2?|ttf|eot|ico|png|jpg|gif|webp)$/i;
8302
8338
  var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
8303
- var SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class(?:Name)?|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset|locale|translation|copy|prose|markdown|slug|handle)/i;
8339
+ var SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class(?:Name)?|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset|chars|alphabet|locale|translation|copy|prose|markdown|slug|handle)/i;
8304
8340
  var HASH_LIKE_VAR_NAMES = /(?:^|[^a-z])(?:hash|digest|checksum|fingerprint|etag|crc|md5|sha1|sha256|sha512|contenthash|buildid|revision|commit|sri|integrity|cacheKey|fileHash|assetId|versionId)(?:$|[^a-z])/i;
8305
8341
  var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
8306
8342
  var SECRET_VAR_NAMES = /(?:^|[^a-z])(?:key|secret|token|password|passwd|pwd|api[_-]?key|auth|credential|private|signing|bearer|access[_-]?token|refresh[_-]?token|session[_-]?id)(?:$|[^a-z])/i;
@@ -8334,6 +8370,7 @@ function scanEntropy(files) {
8334
8370
  let match;
8335
8371
  while ((match = stringPattern.exec(line)) !== null) {
8336
8372
  const value = match[2];
8373
+ if (value.includes("${")) continue;
8337
8374
  if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
8338
8375
  if (HASH_PREFIX_RE.test(value)) continue;
8339
8376
  const beforeAssign = line.substring(0, match.index);