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.js CHANGED
@@ -964,8 +964,16 @@ var sqlInjection = {
964
964
  // The older `[^"']*` class bailed out at the first inner quote and
965
965
  // missed every realistic SQL-containing concat.
966
966
  /(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
967
- // Direct variable interpolation in a SQL verb context
968
- /(?:SELECT|INSERT|UPDATE|DELETE|WHERE)\s+.*\$\{(?!.*parameterized)/gi
967
+ // Direct variable interpolation in a SQL string literal. Requires the
968
+ // verb to START a quoted/backtick literal AND be followed — within the
969
+ // same literal — by a SQL structural keyword (FROM/SET/WHERE/…), then a
970
+ // `${` interpolation. The structural keyword is what distinguishes a
971
+ // real query from ordinary prose: the previous pattern matched any
972
+ // SELECT/UPDATE/… word anywhere on a line followed by `${`, so UI copy
973
+ // like `UPDATE your profile settings ${name}` flagged as a CRITICAL SQL
974
+ // injection. Real queries ("SELECT … FROM …", "UPDATE … SET …") always
975
+ // carry a second structural keyword; sentences almost never do.
976
+ /["'`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^"'`\n]*\b(?:FROM|INTO|SET|VALUES|WHERE|JOIN|RETURNING)\b[^"'`\n]*\$\{(?!.*parameterized)/gi
969
977
  ];
970
978
  const matches = [];
971
979
  const usesParams = /\?\s*,|\?[^?,;]{1,20}\?|\$\d+|:[\w]+|\bprepare\b|\bplaceholders?\b/i.test(content);
@@ -1011,6 +1019,7 @@ var xssVulnerability = {
1011
1019
  // {@html} in Svelte
1012
1020
  /\{@html\s/g
1013
1021
  ];
1022
+ const allLines = content.split("\n");
1014
1023
  const matches = [];
1015
1024
  for (const pattern of patterns) {
1016
1025
  const raw = findMatches(
@@ -1018,12 +1027,18 @@ var xssVulnerability = {
1018
1027
  pattern,
1019
1028
  xssVulnerability,
1020
1029
  filePath,
1021
- () => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)"
1030
+ () => "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."
1022
1031
  );
1023
1032
  for (const m of raw) {
1024
- const lineText = content.split("\n")[m.line - 1] || "";
1033
+ const lineText = allLines[m.line - 1] || "";
1025
1034
  if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
1026
1035
  if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
1036
+ if (/dangerouslySetInnerHTML/.test(lineText)) {
1037
+ const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
1038
+ if (/JSON\.stringify/i.test(windowText)) continue;
1039
+ const lineStartIdx = allLines.slice(0, m.line - 1).reduce((acc, l) => acc + l.length + 1, 0);
1040
+ if (isInlineSilenced(content, lineStartIdx, "VC007")) continue;
1041
+ }
1027
1042
  matches.push(m);
1028
1043
  }
1029
1044
  }
@@ -1299,23 +1314,35 @@ var unvalidatedRedirect = {
1299
1314
  check(content, filePath) {
1300
1315
  if (isTestFile(filePath)) return [];
1301
1316
  if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];
1302
- const patterns = [
1303
- /window\.location\s*=\s*(?!["'`]https?:\/\/)/g,
1304
- /window\.location\.href\s*=\s*(?!["'`]https?:\/\/)/g,
1305
- /window\.location\.assign\s*\(\s*(?!["'`]https?:\/\/)/g,
1306
- /window\.location\.replace\s*\(\s*(?!["'`]https?:\/\/)/g,
1307
- /res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi
1308
- ];
1309
1317
  const matches = [];
1310
- for (const p of patterns) {
1311
- matches.push(...findMatches(
1312
- content,
1313
- p,
1314
- unvalidatedRedirect,
1315
- filePath,
1316
- () => "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly."
1317
- ));
1318
+ const navPattern = /(?:window\.location(?:\.href)?\s*=\s*|window\.location\.(?:assign|replace)\s*\(\s*)([^;\n)]+)/g;
1319
+ let m;
1320
+ while ((m = navPattern.exec(content)) !== null) {
1321
+ if (isCommentLine(content, m.index)) continue;
1322
+ const value = m[1].trim();
1323
+ const hasInterpolation = /\$\{/.test(value);
1324
+ const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
1325
+ if (!hasInterpolation && isPureLiteral) continue;
1326
+ if (isInlineSilenced(content, m.index, "VC016")) continue;
1327
+ const line = content.substring(0, m.index).split("\n").length;
1328
+ matches.push({
1329
+ rule: "VC016",
1330
+ title: unvalidatedRedirect.title,
1331
+ severity: "high",
1332
+ category: "Injection",
1333
+ file: filePath,
1334
+ line,
1335
+ snippet: getSnippet(content, line),
1336
+ 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."
1337
+ });
1318
1338
  }
1339
+ matches.push(...findMatches(
1340
+ content,
1341
+ /res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi,
1342
+ unvalidatedRedirect,
1343
+ filePath,
1344
+ () => "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly."
1345
+ ));
1319
1346
  return matches;
1320
1347
  }
1321
1348
  };
@@ -8035,11 +8062,20 @@ var SAFE_PATTERNS = [
8035
8062
  /^'sha\d+-/,
8036
8063
  /^nonce-/i,
8037
8064
  // Embedded data URIs
8038
- /^data:[a-z]+\//i
8065
+ /^data:[a-z]+\//i,
8066
+ // Sentry DSN — a publishable, client-side ingest key (it ships to the
8067
+ // browser), not a secret. Shape: https://<hash>@o<orgid>.ingest.<region.>
8068
+ // sentry.io/<projectid>. The generic URL allowlist above deliberately
8069
+ // excludes anything with an `@`, so the DSN would otherwise fire.
8070
+ /^https?:\/\/[0-9a-f]+@o\d+\.ingest\.[a-z0-9.]*sentry\.io\//i,
8071
+ // HTTP security-header directive values (HSTS, etc.) — `max-age=63072000;
8072
+ // includeSubDomains; preload`. Config strings a security scanner actively
8073
+ // tells users to add; never secrets.
8074
+ /^max-age=\d+/i
8039
8075
  ];
8040
8076
  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;
8041
8077
  var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
8042
- 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;
8078
+ 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;
8043
8079
  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;
8044
8080
  var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
8045
8081
  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;
@@ -8073,6 +8109,7 @@ function scanEntropy(files) {
8073
8109
  let match;
8074
8110
  while ((match = stringPattern.exec(line)) !== null) {
8075
8111
  const value = match[2];
8112
+ if (value.includes("${")) continue;
8076
8113
  if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
8077
8114
  if (HASH_PREFIX_RE.test(value)) continue;
8078
8115
  const beforeAssign = line.substring(0, match.index);