xploitscan-shared-rules 1.12.0 → 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;
@@ -1527,9 +1527,10 @@ var xssVulnerability = {
1527
1527
  category: "Injection",
1528
1528
  description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
1529
1529
  check(content, filePath) {
1530
+ const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
1530
1531
  if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
1531
- if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
1532
- 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 [];
1533
1534
  const patterns = [
1534
1535
  // React dangerouslySetInnerHTML
1535
1536
  /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
@@ -1540,7 +1541,12 @@ var xssVulnerability = {
1540
1541
  // v-html in Vue
1541
1542
  /v-html\s*=/g,
1542
1543
  // {@html} in Svelte
1543
- /\{@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
1544
1550
  ];
1545
1551
  const allLines = content.split("\n");
1546
1552
  const matches = [];
@@ -4283,17 +4289,37 @@ var ssti = {
4283
4289
  (call, line) => {
4284
4290
  const first = call.arguments[0];
4285
4291
  if (!first || first.type === "SpreadElement") return;
4286
- if (!taint.isTainted(first)) return;
4287
- if (matches.some((m) => m.line === line)) return;
4288
- matches.push(
4289
- astMatch(
4290
- content,
4291
- filePath,
4292
- line,
4293
- ssti,
4294
- "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
4295
- )
4296
- );
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
+ }
4297
4323
  }
4298
4324
  );
4299
4325
  return filterSilenced(matches, content, "VC082");
@@ -4556,16 +4582,61 @@ var unprotectedDownload = {
4556
4582
  if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
4557
4583
  if (!isServerSideFile(filePath, content)) return [];
4558
4584
  const matches = [];
4559
- if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4560
- const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
4561
- if (!hasValidation) {
4562
- matches.push(...findMatches(
4563
- content,
4564
- /(?:sendFile|download|send_file)\s*\(/gi,
4565
- unprotectedDownload,
4566
- filePath,
4567
- () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
4568
- ));
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
+ );
4569
4640
  }
4570
4641
  }
4571
4642
  return matches;
@@ -5453,7 +5524,7 @@ var pathTraversal = {
5453
5524
  while ((m = pat.exec(content)) !== null) {
5454
5525
  if (isCommentLine(content, m.index)) continue;
5455
5526
  const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
5456
- if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
5527
+ if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
5457
5528
  const lineNum = content.substring(0, m.index).split("\n").length;
5458
5529
  findings.push({
5459
5530
  rule: "VC117",