xploitscan-shared-rules 1.9.0 → 1.11.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
@@ -635,6 +635,77 @@ function callPreservesTaint(node, rec) {
635
635
  }
636
636
  return false;
637
637
  }
638
+ function isParamDerived(n, p) {
639
+ if (!n) return false;
640
+ if (n.type === "Identifier") return n.name === p;
641
+ if (n.type === "MemberExpression") return isParamDerived(n.object, p);
642
+ return false;
643
+ }
644
+ function formattedFlow(n, p) {
645
+ if (!n) return false;
646
+ switch (n.type) {
647
+ case "TemplateLiteral":
648
+ return n.expressions.some(
649
+ (e) => isParamDerived(e, p) || formattedFlow(e, p)
650
+ );
651
+ case "BinaryExpression":
652
+ if (n.operator !== "+") return false;
653
+ return isParamDerived(n.left, p) || isParamDerived(n.right, p) || formattedFlow(n.left, p) || formattedFlow(n.right, p);
654
+ case "ObjectExpression":
655
+ return n.properties.some(
656
+ (pr) => pr.type === "SpreadElement" && isParamDerived(pr.argument, p)
657
+ );
658
+ case "ArrayExpression":
659
+ return n.elements.some(
660
+ (el) => el != null && el.type === "SpreadElement" && isParamDerived(el.argument, p)
661
+ );
662
+ case "ConditionalExpression":
663
+ return formattedFlow(n.consequent, p) || formattedFlow(n.alternate, p);
664
+ case "AwaitExpression":
665
+ return formattedFlow(n.argument, p);
666
+ default:
667
+ return false;
668
+ }
669
+ }
670
+ function collectReturnArgs(fn) {
671
+ const out = [];
672
+ if (fn.type === "ArrowFunctionExpression" && fn.body.type !== "BlockStatement") {
673
+ out.push(fn.body);
674
+ return out;
675
+ }
676
+ const visit = (n) => {
677
+ if (!n || typeof n !== "object") return;
678
+ const node = n;
679
+ if (typeof node.type !== "string") return;
680
+ if (node.type === "ReturnStatement") {
681
+ const arg = node.argument;
682
+ if (arg) out.push(arg);
683
+ return;
684
+ }
685
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
686
+ return;
687
+ }
688
+ for (const key of Object.keys(node)) {
689
+ const v = node[key];
690
+ if (Array.isArray(v)) v.forEach(visit);
691
+ else visit(v);
692
+ }
693
+ };
694
+ visit(fn.body);
695
+ return out;
696
+ }
697
+ function computePassthrough(fn) {
698
+ const params = fn.params ?? [];
699
+ const returns = collectReturnArgs(fn);
700
+ const out = /* @__PURE__ */ new Set();
701
+ for (let i = 0; i < params.length; i++) {
702
+ const param = params[i];
703
+ if (param.type !== "Identifier") continue;
704
+ const name = param.name;
705
+ if (returns.some((r) => formattedFlow(r, name))) out.add(i);
706
+ }
707
+ return out;
708
+ }
638
709
  function buildTaintMap(parsed) {
639
710
  const tainted = /* @__PURE__ */ new Set();
640
711
  function markPatternTainted(target) {
@@ -728,6 +799,7 @@ function buildTaintMap(parsed) {
728
799
  if (node.type === "CallExpression") {
729
800
  if (nodeIsTaintedSource(node)) return true;
730
801
  if (callPreservesTaint(node, exprIsTainted)) return true;
802
+ if (localCallPropagates(node, exprIsTainted)) return true;
731
803
  if (node.callee.type === "MemberExpression") {
732
804
  if (exprIsTainted(node.callee.object)) return true;
733
805
  const obj = node.callee.object;
@@ -760,6 +832,34 @@ function buildTaintMap(parsed) {
760
832
  }
761
833
  return false;
762
834
  }
835
+ const fnPassthrough = /* @__PURE__ */ new Map();
836
+ const recordFn = (name, fn) => {
837
+ const pt = computePassthrough(fn);
838
+ if (pt.size > 0) fnPassthrough.set(name, pt);
839
+ };
840
+ traverse(parsed.ast, {
841
+ FunctionDeclaration(path) {
842
+ const n = path.node;
843
+ if (n.type === "FunctionDeclaration" && n.id && n.id.type === "Identifier") {
844
+ recordFn(n.id.name, n);
845
+ }
846
+ },
847
+ VariableDeclarator(path) {
848
+ const n = path.node;
849
+ if (n.type !== "VariableDeclarator" || n.id.type !== "Identifier" || !n.init) return;
850
+ if (n.init.type === "ArrowFunctionExpression" || n.init.type === "FunctionExpression") {
851
+ recordFn(n.id.name, n.init);
852
+ }
853
+ }
854
+ });
855
+ function localCallPropagates(node, rec) {
856
+ if (node.callee.type !== "Identifier") return false;
857
+ const pt = fnPassthrough.get(node.callee.name);
858
+ if (!pt) return false;
859
+ return node.arguments.some(
860
+ (a, i) => pt.has(i) && a.type !== "SpreadElement" && rec(a)
861
+ );
862
+ }
763
863
  traverse(parsed.ast, {
764
864
  VariableDeclarator(path) {
765
865
  const node = path.node;
@@ -844,6 +944,7 @@ function buildTaintMap(parsed) {
844
944
  }
845
945
  if (node.type === "CallExpression") {
846
946
  if (callPreservesTaint(node, isTainted)) return true;
947
+ if (localCallPropagates(node, isTainted)) return true;
847
948
  if (node.callee.type === "MemberExpression") {
848
949
  if (isTainted(node.callee.object)) return true;
849
950
  const obj = node.callee.object;
@@ -979,9 +1080,12 @@ var SERVER_SIDE_PATH_RE = new RegExp(
979
1080
  ].join("|"),
980
1081
  "i"
981
1082
  );
982
- function isServerSideFile(filePath) {
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/;
1084
+ function isServerSideFile(filePath, content) {
983
1085
  if (isTestFile(filePath)) return false;
984
- return SERVER_SIDE_PATH_RE.test(filePath);
1086
+ if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
1087
+ if (content && SERVER_SIDE_CONTENT_RE.test(content)) return true;
1088
+ return false;
985
1089
  }
986
1090
  var CONFIG_FILE_PATTERN = new RegExp(
987
1091
  [
@@ -1194,7 +1298,7 @@ var missingAuthMiddleware = {
1194
1298
  category: "Authentication",
1195
1299
  description: "API routes without authentication checks allow unauthorized access.",
1196
1300
  check(content, filePath) {
1197
- if (!isServerSideFile(filePath)) return [];
1301
+ if (!isServerSideFile(filePath, content)) return [];
1198
1302
  const routePatterns = [
1199
1303
  // Express/Hono style
1200
1304
  /\.(get|post|put|patch|delete)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?:async\s+)?\(?(?:req|c|ctx)/gi,
@@ -2512,7 +2616,7 @@ var exposedStackTraces = {
2512
2616
  category: "Information Leakage",
2513
2617
  description: "Returning error.stack or detailed error messages in API responses reveals internal code paths, file structure, and dependencies to attackers.",
2514
2618
  check(content, filePath) {
2515
- if (!isServerSideFile(filePath)) return [];
2619
+ if (!isServerSideFile(filePath, content)) return [];
2516
2620
  const matches = [];
2517
2621
  const patterns = [
2518
2622
  // Sending stack trace in response
@@ -2610,7 +2714,7 @@ var ssrfVulnerability = {
2610
2714
  category: "Injection",
2611
2715
  description: "Fetching URLs from user input without validation allows attackers to access internal services, cloud metadata endpoints (169.254.169.254), and private networks.",
2612
2716
  check(content, filePath) {
2613
- if (!isServerSideFile(filePath)) return [];
2717
+ if (!isServerSideFile(filePath, content)) return [];
2614
2718
  const matches = [];
2615
2719
  const hasValidation = /allowedHosts|allowedDomains|allowedUrls|safeDomain|whitelist|urlValidator|new URL.*hostname.*includes|isAllowedUrl|validateUrl|isValidUrl/i.test(content);
2616
2720
  if (hasValidation) return [];
@@ -2667,7 +2771,7 @@ var massAssignment = {
2667
2771
  category: "Authorization",
2668
2772
  description: "Spreading or assigning request body directly into database models allows attackers to set fields they shouldn't (e.g., isAdmin, role, verified).",
2669
2773
  check(content, filePath) {
2670
- if (!isServerSideFile(filePath)) return [];
2774
+ if (!isServerSideFile(filePath, content)) return [];
2671
2775
  const hasSanitization = /pick\(|omit\(|allowedFields|sanitize|whitelist|permit|strong_params/i.test(content);
2672
2776
  if (hasSanitization) return [];
2673
2777
  const matches = [];
@@ -2819,7 +2923,7 @@ var logInjection = {
2819
2923
  category: "Injection",
2820
2924
  description: "Logging unsanitized user input allows attackers to forge log entries, inject malicious content, or exploit log aggregation systems via newlines and special characters.",
2821
2925
  check(content, filePath) {
2822
- if (!isServerSideFile(filePath)) return [];
2926
+ if (!isServerSideFile(filePath, content)) return [];
2823
2927
  const hasSanitization = /replace\s*\(\s*\/\[?\\r\\n\]|sanitizeLog|stripNewlines|sanitizeForLog/i.test(content);
2824
2928
  if (hasSanitization) return [];
2825
2929
  const matches = [];
@@ -2885,7 +2989,7 @@ var weakPasswordRequirements = {
2885
2989
  if (!/(?:password|passwd|pwd)/i.test(content)) return [];
2886
2990
  const isPasswordContext = /(?:register|signup|sign.up|createUser|create.user|changePassword|resetPassword|set.password|validatePassword|validate.password|passwordPolicy|password.policy)/i.test(
2887
2991
  content
2888
- ) || isServerSideFile(filePath);
2992
+ ) || isServerSideFile(filePath, content);
2889
2993
  const matches = [];
2890
2994
  if (isPasswordContext) {
2891
2995
  const weakThresholdPatterns = [
@@ -4039,8 +4143,8 @@ var xxeVulnerability = {
4039
4143
  if (!/xml|parseXml|parseXML|DOMParser|SAXParser|etree|lxml|libxml/i.test(content)) return [];
4040
4144
  const matches = [];
4041
4145
  const patterns = [
4042
- /\.parseXm?l\s*\(/gi,
4043
- // catches parseXml (libxmljs) AND parseXML
4146
+ /\.parseXm?l(?:String)?\s*\(/gi,
4147
+ // parseXml / parseXML / parseXmlString (libxmljs)
4044
4148
  // NOTE: the browser `new DOMParser()` is intentionally NOT flagged.
4045
4149
  // Per the HTML/XML spec, DOMParser.parseFromString does not resolve
4046
4150
  // external entities, so it is not an XXE sink — flagging it produced a
@@ -4226,7 +4330,7 @@ var exposedAdminRoutes = {
4226
4330
  category: "Information Leakage",
4227
4331
  description: "Routes like /admin, /debug, /phpinfo, or /actuator without authentication expose sensitive controls and information to attackers.",
4228
4332
  check(content, filePath) {
4229
- if (!isServerSideFile(filePath)) return [];
4333
+ if (!isServerSideFile(filePath, content)) return [];
4230
4334
  const matches = [];
4231
4335
  const patterns = [
4232
4336
  /[.'"]\s*(?:get|use|all)\s*\(\s*["'`]\/(?:admin|debug|_debug|__debug__|phpinfo|actuator|graphiql|playground|swagger|reset|seed|test|dev|mock)["'`]/gi
@@ -4318,7 +4422,7 @@ var missingContentDisposition = {
4318
4422
  description: "File download endpoints without Content-Disposition headers may render files inline, leading to XSS if the file contains HTML/JS.",
4319
4423
  check(content, filePath) {
4320
4424
  if (!/(?:download|sendFile|send_file|pipe|createReadStream)/i.test(content)) return [];
4321
- if (!isServerSideFile(filePath)) return [];
4425
+ if (!isServerSideFile(filePath, content)) return [];
4322
4426
  if (/Content-Disposition|attachment|download/i.test(content)) return [];
4323
4427
  return findMatches(
4324
4428
  content,
@@ -4415,7 +4519,7 @@ var unprotectedDownload = {
4415
4519
  description: "File download endpoints that accept user-controlled filenames without path validation allow directory traversal to read arbitrary files.",
4416
4520
  check(content, filePath) {
4417
4521
  if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
4418
- if (!isServerSideFile(filePath)) return [];
4522
+ if (!isServerSideFile(filePath, content)) return [];
4419
4523
  const matches = [];
4420
4524
  if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4421
4525
  const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
@@ -4444,7 +4548,7 @@ var commandInjection = {
4444
4548
  const matches = [];
4445
4549
  const patterns = [
4446
4550
  // Node.js — require standalone exec/execSync, not db.exec() or conn.exec()
4447
- /(?<![.\w])(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
4551
+ /(?<![.\w])(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*["']\s*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
4448
4552
  /child_process.*exec\s*\(\s*(?!["'`])/g,
4449
4553
  // spawn / execFile / exec with shell: true AND a template literal
4450
4554
  // first arg. `shell: true` converts the first argument into a string