xploitscan-shared-rules 1.7.0 → 1.7.3

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
  };
@@ -2115,9 +2142,15 @@ var insecureDeserialization = {
2115
2142
  () => "Never deserialize untrusted data. Use JSON instead of pickle/Marshal/unserialize. For YAML, use yaml.safe_load(). Validate and sanitize all input before deserialization."
2116
2143
  ));
2117
2144
  }
2145
+ const isJsTs = /\.(jsx?|tsx?|mjs|cjs)$/.test(filePath);
2118
2146
  return matches.filter((m) => {
2119
2147
  if (!/yaml\.load\s*\(/.test(m.snippet ?? "")) return true;
2120
2148
  const lineText = (m.snippet ?? "").toLowerCase();
2149
+ if (isJsTs) {
2150
+ const ctxLines = content.split("\n").slice(m.line - 1, m.line + 2).join("\n");
2151
+ if (/default_full_schema|\bfull_schema\b/i.test(ctxLines)) return true;
2152
+ return false;
2153
+ }
2121
2154
  if (/safe_schema|failsafe_schema|safe_load|safeloader/.test(lineText)) {
2122
2155
  return false;
2123
2156
  }
@@ -3885,7 +3918,11 @@ var xxeVulnerability = {
3885
3918
  const patterns = [
3886
3919
  /\.parseXm?l\s*\(/gi,
3887
3920
  // catches parseXml (libxmljs) AND parseXML
3888
- /new\s+DOMParser\s*\(\)/g,
3921
+ // NOTE: the browser `new DOMParser()` is intentionally NOT flagged.
3922
+ // Per the HTML/XML spec, DOMParser.parseFromString does not resolve
3923
+ // external entities, so it is not an XXE sink — flagging it produced a
3924
+ // critical false positive on ordinary client-side XML/HTML parsing.
3925
+ // Real XXE sinks (libxmljs parseXml with noent, etree, SAX) remain below.
3889
3926
  /etree\.parse\s*\(/g,
3890
3927
  /lxml\.etree/g,
3891
3928
  /SAXParserFactory/g,
@@ -7056,7 +7093,11 @@ var llmOutputAsHTML = {
7056
7093
  const findings = [];
7057
7094
  const patterns = [
7058
7095
  // dangerouslySetInnerHTML with .choices[0].message.content / .text / etc.
7059
- /dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:\s*[^}]*\b(?:choices\[\d*\]?\.message|completion|response|message\.content|content_block|delta\.text|generated_text|output_text|text)\b/g,
7096
+ // NOTE: a bare `text` token used to be in this alternation and matched
7097
+ // any `.text` property (e.g. `post.text`) in a file that merely imported
7098
+ // an LLM SDK — a high-severity false positive. Only LLM-specific shapes
7099
+ // remain (delta.text / output_text / generated_text are qualified).
7100
+ /dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:\s*[^}]*\b(?:choices\[\d*\]?\.message|completion|response|message\.content|content_block|delta\.text|generated_text|output_text)\b/g,
7060
7101
  // .innerHTML = response.choices[0].message.content
7061
7102
  /\.innerHTML\s*=\s*[^;]*\b(?:choices\[\d*\]?\.message|completion|response\.message|message\.content|delta\.text|generated_text|output_text)\b/g
7062
7103
  ];
@@ -8296,11 +8337,20 @@ var SAFE_PATTERNS = [
8296
8337
  /^'sha\d+-/,
8297
8338
  /^nonce-/i,
8298
8339
  // Embedded data URIs
8299
- /^data:[a-z]+\//i
8340
+ /^data:[a-z]+\//i,
8341
+ // Sentry DSN — a publishable, client-side ingest key (it ships to the
8342
+ // browser), not a secret. Shape: https://<hash>@o<orgid>.ingest.<region.>
8343
+ // sentry.io/<projectid>. The generic URL allowlist above deliberately
8344
+ // excludes anything with an `@`, so the DSN would otherwise fire.
8345
+ /^https?:\/\/[0-9a-f]+@o\d+\.ingest\.[a-z0-9.]*sentry\.io\//i,
8346
+ // HTTP security-header directive values (HSTS, etc.) — `max-age=63072000;
8347
+ // includeSubDomains; preload`. Config strings a security scanner actively
8348
+ // tells users to add; never secrets.
8349
+ /^max-age=\d+/i
8300
8350
  ];
8301
8351
  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
8352
  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;
8353
+ 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
8354
  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
8355
  var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
8306
8356
  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 +8384,7 @@ function scanEntropy(files) {
8334
8384
  let match;
8335
8385
  while ((match = stringPattern.exec(line)) !== null) {
8336
8386
  const value = match[2];
8387
+ if (value.includes("${")) continue;
8337
8388
  if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
8338
8389
  if (HASH_PREFIX_RE.test(value)) continue;
8339
8390
  const beforeAssign = line.substring(0, match.index);