xploitscan-shared-rules 1.11.1 → 1.13.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.
package/dist/index.cjs CHANGED
@@ -1080,7 +1080,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
1080
1080
  ].join("|"),
1081
1081
  "i"
1082
1082
  );
1083
- var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b/;
1083
+ var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b|(?:import|require)\b[^;\n]*['"]express['"]|\bNextFunction\b/;
1084
1084
  function isServerSideFile(filePath, content) {
1085
1085
  if (isTestFile(filePath)) return false;
1086
1086
  if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
@@ -1482,6 +1482,41 @@ var sqlInjection = {
1482
1482
  matches.push(m);
1483
1483
  }
1484
1484
  }
1485
+ if (/\.\s*(?:query|execute|raw|queryRawUnsafe|executeRawUnsafe|\$queryRawUnsafe|\$executeRawUnsafe)\s*\(/.test(content)) {
1486
+ const ctx = tryParse(content, filePath);
1487
+ if (ctx) {
1488
+ const { parsed, taint } = ctx;
1489
+ const SQL_SINKS = /* @__PURE__ */ new Set([
1490
+ "query",
1491
+ "execute",
1492
+ "raw",
1493
+ "queryRawUnsafe",
1494
+ "executeRawUnsafe",
1495
+ "$queryRawUnsafe",
1496
+ "$executeRawUnsafe"
1497
+ ]);
1498
+ visitCalls(
1499
+ parsed,
1500
+ (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && SQL_SINKS.has(callee.property.name),
1501
+ (call, line) => {
1502
+ const first = call.arguments[0];
1503
+ if (!first || first.type === "SpreadElement") return;
1504
+ if (first.type === "StringLiteral" || first.type === "TemplateLiteral") return;
1505
+ if (!taint.isTainted(first)) return;
1506
+ if (matches.some((m) => m.line === line)) return;
1507
+ matches.push(
1508
+ astMatch(
1509
+ content,
1510
+ filePath,
1511
+ line,
1512
+ sqlInjection,
1513
+ "Use parameterized queries instead of building SQL from user input in a variable. Example: db.query('SELECT * FROM users WHERE login = ?', [req.body.login])"
1514
+ )
1515
+ );
1516
+ }
1517
+ );
1518
+ }
1519
+ }
1485
1520
  return matches;
1486
1521
  }
1487
1522
  };
@@ -1492,9 +1527,10 @@ var xssVulnerability = {
1492
1527
  category: "Injection",
1493
1528
  description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
1494
1529
  check(content, filePath) {
1530
+ const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
1495
1531
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
1496
- if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1497
- if (/(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1532
+ if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1533
+ if (!hasSanitizerBypass && /(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
1498
1534
  const patterns = [
1499
1535
  // React dangerouslySetInnerHTML
1500
1536
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -1505,7 +1541,12 @@ var xssVulnerability = {
1505
1541
  // v-html in Vue
1506
1542
  /v-html\s*=/g,
1507
1543
  // {@html} in Svelte
1508
- /\{@html\s/g
1544
+ /\{@html\s/g,
1545
+ // Angular DomSanitizer escape hatches — bypassSecurityTrustHtml /
1546
+ // bypassSecurityTrustScript / ...Url / ...ResourceUrl / ...Style. These
1547
+ // mark a value as trusted and skip Angular's built-in sanitization;
1548
+ // passing user input through one is the canonical Angular DOM-XSS sink.
1549
+ /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/g
1509
1550
  ];
1510
1551
  const allLines = content.split("\n");
1511
1552
  const matches = [];
@@ -2726,12 +2767,12 @@ var ssrfVulnerability = {
2726
2767
  filePath,
2727
2768
  () => "Validate URLs against an allowlist before fetching. Block internal IPs: 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254 (cloud metadata). Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');"
2728
2769
  ));
2729
- if (!/\b(?:fetch|axios|got|request|http\.get|https\.get)\b/.test(content)) return matches;
2770
+ if (!/\b(?:fetch|axios|got|request|needle|superagent|undici|http\.get|https\.get)\b/.test(content)) return matches;
2730
2771
  const ctx = tryParse(content, filePath);
2731
2772
  if (!ctx) return matches;
2732
2773
  const { parsed, taint } = ctx;
2733
- const FETCH_CALLEES = /* @__PURE__ */ new Set(["fetch", "axios", "got", "request"]);
2734
- const FETCH_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "request"]);
2774
+ const FETCH_CALLEES = /* @__PURE__ */ new Set(["fetch", "axios", "got", "request", "needle", "superagent", "undici"]);
2775
+ const FETCH_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "request", "head"]);
2735
2776
  visitCalls(
2736
2777
  parsed,
2737
2778
  (callee) => {
@@ -2740,7 +2781,7 @@ var ssrfVulnerability = {
2740
2781
  if (!FETCH_METHODS.has(callee.property.name)) return false;
2741
2782
  const obj = callee.object;
2742
2783
  if (obj.type === "Identifier") {
2743
- return obj.name === "axios" || obj.name === "got" || obj.name === "http" || obj.name === "https";
2784
+ return obj.name === "axios" || obj.name === "got" || obj.name === "http" || obj.name === "https" || obj.name === "needle" || obj.name === "superagent" || obj.name === "undici";
2744
2785
  }
2745
2786
  }
2746
2787
  return false;
@@ -4248,17 +4289,37 @@ var ssti = {
4248
4289
  (call, line) => {
4249
4290
  const first = call.arguments[0];
4250
4291
  if (!first || first.type === "SpreadElement") return;
4251
- if (!taint.isTainted(first)) return;
4252
- if (matches.some((m) => m.line === line)) return;
4253
- matches.push(
4254
- astMatch(
4255
- content,
4256
- filePath,
4257
- line,
4258
- ssti,
4259
- "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
4260
- )
4261
- );
4292
+ if (taint.isTainted(first)) {
4293
+ if (matches.some((m) => m.line === line)) return;
4294
+ matches.push(
4295
+ astMatch(
4296
+ content,
4297
+ filePath,
4298
+ line,
4299
+ ssti,
4300
+ "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
4301
+ )
4302
+ );
4303
+ return;
4304
+ }
4305
+ const opts = call.arguments[1];
4306
+ if (opts && opts.type === "ObjectExpression") {
4307
+ const hasTaintedSpread = opts.properties.some(
4308
+ (p) => p.type === "SpreadElement" && taint.isTainted(p.argument)
4309
+ );
4310
+ if (hasTaintedSpread) {
4311
+ if (matches.some((m) => m.line === line)) return;
4312
+ matches.push(
4313
+ astMatch(
4314
+ content,
4315
+ filePath,
4316
+ line,
4317
+ ssti,
4318
+ "Don't spread req.body/req.query into template locals \u2014 an attacker can inject reserved view options (e.g. `layout`) to load arbitrary templates. Pass only the specific values the view needs: render('view', { name: req.body.name })."
4319
+ )
4320
+ );
4321
+ }
4322
+ }
4262
4323
  }
4263
4324
  );
4264
4325
  return filterSilenced(matches, content, "VC082");
@@ -4521,16 +4582,61 @@ var unprotectedDownload = {
4521
4582
  if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
4522
4583
  if (!isServerSideFile(filePath, content)) return [];
4523
4584
  const matches = [];
4524
- if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4525
- const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
4526
- if (!hasValidation) {
4527
- matches.push(...findMatches(
4528
- content,
4529
- /(?:sendFile|download|send_file)\s*\(/gi,
4530
- unprotectedDownload,
4531
- filePath,
4532
- () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
4533
- ));
4585
+ const hasContainmentCheck = /\.startsWith\s*\(/.test(content) || /realpath/i.test(content) || /includes\s*\(\s*["']\.\./.test(content);
4586
+ if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content) && !hasContainmentCheck) {
4587
+ matches.push(...findMatches(
4588
+ content,
4589
+ /(?:sendFile|download|send_file)\s*\(/gi,
4590
+ unprotectedDownload,
4591
+ filePath,
4592
+ () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
4593
+ ));
4594
+ }
4595
+ if (!hasContainmentCheck && /\b(?:sendFile|download)\s*\(/.test(content)) {
4596
+ const ctx = tryParse(content, filePath);
4597
+ if (ctx) {
4598
+ const { parsed, taint } = ctx;
4599
+ const readsUserInput = /\b(?:req|request)\s*\.\s*(?:params|query|body|headers|cookies)\b/.test(content) || /\bparams\s*\.\s*\w/.test(content) || /\bquery\s*\.\s*\w/.test(content) || /\breq\.body\b/.test(content);
4600
+ const isUnsafePathBuild = (node) => {
4601
+ if (node.type !== "CallExpression") return false;
4602
+ const callee = node.callee;
4603
+ const isPathBuild = callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && (callee.property.name === "resolve" || callee.property.name === "join");
4604
+ if (!isPathBuild) return false;
4605
+ const rootedAtDirname = node.arguments.some(
4606
+ (a) => a.type === "Identifier" && (a.name === "__dirname" || a.name === "__filename")
4607
+ );
4608
+ if (rootedAtDirname) return false;
4609
+ return node.arguments.some(
4610
+ (a) => a.type !== "SpreadElement" && a.type !== "StringLiteral" && a.type !== "TemplateLiteral"
4611
+ );
4612
+ };
4613
+ const argIsTainted = (node) => {
4614
+ if (taint.isTainted(node)) return true;
4615
+ if (readsUserInput && isUnsafePathBuild(node)) return true;
4616
+ if (node.type === "CallExpression") {
4617
+ return node.arguments.some((a) => a.type !== "SpreadElement" && argIsTainted(a));
4618
+ }
4619
+ return false;
4620
+ };
4621
+ visitCalls(
4622
+ parsed,
4623
+ (callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && (callee.property.name === "sendFile" || callee.property.name === "download"),
4624
+ (call, line) => {
4625
+ const first = call.arguments[0];
4626
+ if (!first || first.type === "SpreadElement") return;
4627
+ if (!argIsTainted(first)) return;
4628
+ if (matches.some((m) => m.line === line)) return;
4629
+ matches.push(
4630
+ astMatch(
4631
+ content,
4632
+ filePath,
4633
+ line,
4634
+ unprotectedDownload,
4635
+ "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
4636
+ )
4637
+ );
4638
+ }
4639
+ );
4534
4640
  }
4535
4641
  }
4536
4642
  return matches;
@@ -5418,7 +5524,7 @@ var pathTraversal = {
5418
5524
  while ((m = pat.exec(content)) !== null) {
5419
5525
  if (isCommentLine(content, m.index)) continue;
5420
5526
  const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
5421
- if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
5527
+ if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
5422
5528
  const lineNum = content.substring(0, m.index).split("\n").length;
5423
5529
  findings.push({
5424
5530
  rule: "VC117",