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.js CHANGED
@@ -370,6 +370,77 @@ function callPreservesTaint(node, rec) {
370
370
  }
371
371
  return false;
372
372
  }
373
+ function isParamDerived(n, p) {
374
+ if (!n) return false;
375
+ if (n.type === "Identifier") return n.name === p;
376
+ if (n.type === "MemberExpression") return isParamDerived(n.object, p);
377
+ return false;
378
+ }
379
+ function formattedFlow(n, p) {
380
+ if (!n) return false;
381
+ switch (n.type) {
382
+ case "TemplateLiteral":
383
+ return n.expressions.some(
384
+ (e) => isParamDerived(e, p) || formattedFlow(e, p)
385
+ );
386
+ case "BinaryExpression":
387
+ if (n.operator !== "+") return false;
388
+ return isParamDerived(n.left, p) || isParamDerived(n.right, p) || formattedFlow(n.left, p) || formattedFlow(n.right, p);
389
+ case "ObjectExpression":
390
+ return n.properties.some(
391
+ (pr) => pr.type === "SpreadElement" && isParamDerived(pr.argument, p)
392
+ );
393
+ case "ArrayExpression":
394
+ return n.elements.some(
395
+ (el) => el != null && el.type === "SpreadElement" && isParamDerived(el.argument, p)
396
+ );
397
+ case "ConditionalExpression":
398
+ return formattedFlow(n.consequent, p) || formattedFlow(n.alternate, p);
399
+ case "AwaitExpression":
400
+ return formattedFlow(n.argument, p);
401
+ default:
402
+ return false;
403
+ }
404
+ }
405
+ function collectReturnArgs(fn) {
406
+ const out = [];
407
+ if (fn.type === "ArrowFunctionExpression" && fn.body.type !== "BlockStatement") {
408
+ out.push(fn.body);
409
+ return out;
410
+ }
411
+ const visit = (n) => {
412
+ if (!n || typeof n !== "object") return;
413
+ const node = n;
414
+ if (typeof node.type !== "string") return;
415
+ if (node.type === "ReturnStatement") {
416
+ const arg = node.argument;
417
+ if (arg) out.push(arg);
418
+ return;
419
+ }
420
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
421
+ return;
422
+ }
423
+ for (const key of Object.keys(node)) {
424
+ const v = node[key];
425
+ if (Array.isArray(v)) v.forEach(visit);
426
+ else visit(v);
427
+ }
428
+ };
429
+ visit(fn.body);
430
+ return out;
431
+ }
432
+ function computePassthrough(fn) {
433
+ const params = fn.params ?? [];
434
+ const returns = collectReturnArgs(fn);
435
+ const out = /* @__PURE__ */ new Set();
436
+ for (let i = 0; i < params.length; i++) {
437
+ const param = params[i];
438
+ if (param.type !== "Identifier") continue;
439
+ const name = param.name;
440
+ if (returns.some((r) => formattedFlow(r, name))) out.add(i);
441
+ }
442
+ return out;
443
+ }
373
444
  function buildTaintMap(parsed) {
374
445
  const tainted = /* @__PURE__ */ new Set();
375
446
  function markPatternTainted(target) {
@@ -463,6 +534,7 @@ function buildTaintMap(parsed) {
463
534
  if (node.type === "CallExpression") {
464
535
  if (nodeIsTaintedSource(node)) return true;
465
536
  if (callPreservesTaint(node, exprIsTainted)) return true;
537
+ if (localCallPropagates(node, exprIsTainted)) return true;
466
538
  if (node.callee.type === "MemberExpression") {
467
539
  if (exprIsTainted(node.callee.object)) return true;
468
540
  const obj = node.callee.object;
@@ -495,6 +567,34 @@ function buildTaintMap(parsed) {
495
567
  }
496
568
  return false;
497
569
  }
570
+ const fnPassthrough = /* @__PURE__ */ new Map();
571
+ const recordFn = (name, fn) => {
572
+ const pt = computePassthrough(fn);
573
+ if (pt.size > 0) fnPassthrough.set(name, pt);
574
+ };
575
+ traverse(parsed.ast, {
576
+ FunctionDeclaration(path) {
577
+ const n = path.node;
578
+ if (n.type === "FunctionDeclaration" && n.id && n.id.type === "Identifier") {
579
+ recordFn(n.id.name, n);
580
+ }
581
+ },
582
+ VariableDeclarator(path) {
583
+ const n = path.node;
584
+ if (n.type !== "VariableDeclarator" || n.id.type !== "Identifier" || !n.init) return;
585
+ if (n.init.type === "ArrowFunctionExpression" || n.init.type === "FunctionExpression") {
586
+ recordFn(n.id.name, n.init);
587
+ }
588
+ }
589
+ });
590
+ function localCallPropagates(node, rec) {
591
+ if (node.callee.type !== "Identifier") return false;
592
+ const pt = fnPassthrough.get(node.callee.name);
593
+ if (!pt) return false;
594
+ return node.arguments.some(
595
+ (a, i) => pt.has(i) && a.type !== "SpreadElement" && rec(a)
596
+ );
597
+ }
498
598
  traverse(parsed.ast, {
499
599
  VariableDeclarator(path) {
500
600
  const node = path.node;
@@ -579,6 +679,7 @@ function buildTaintMap(parsed) {
579
679
  }
580
680
  if (node.type === "CallExpression") {
581
681
  if (callPreservesTaint(node, isTainted)) return true;
682
+ if (localCallPropagates(node, isTainted)) return true;
582
683
  if (node.callee.type === "MemberExpression") {
583
684
  if (isTainted(node.callee.object)) return true;
584
685
  const obj = node.callee.object;
@@ -714,9 +815,12 @@ var SERVER_SIDE_PATH_RE = new RegExp(
714
815
  ].join("|"),
715
816
  "i"
716
817
  );
717
- function isServerSideFile(filePath) {
818
+ 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/;
819
+ function isServerSideFile(filePath, content) {
718
820
  if (isTestFile(filePath)) return false;
719
- return SERVER_SIDE_PATH_RE.test(filePath);
821
+ if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
822
+ if (content && SERVER_SIDE_CONTENT_RE.test(content)) return true;
823
+ return false;
720
824
  }
721
825
  var CONFIG_FILE_PATTERN = new RegExp(
722
826
  [
@@ -929,7 +1033,7 @@ var missingAuthMiddleware = {
929
1033
  category: "Authentication",
930
1034
  description: "API routes without authentication checks allow unauthorized access.",
931
1035
  check(content, filePath) {
932
- if (!isServerSideFile(filePath)) return [];
1036
+ if (!isServerSideFile(filePath, content)) return [];
933
1037
  const routePatterns = [
934
1038
  // Express/Hono style
935
1039
  /\.(get|post|put|patch|delete)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?:async\s+)?\(?(?:req|c|ctx)/gi,
@@ -2247,7 +2351,7 @@ var exposedStackTraces = {
2247
2351
  category: "Information Leakage",
2248
2352
  description: "Returning error.stack or detailed error messages in API responses reveals internal code paths, file structure, and dependencies to attackers.",
2249
2353
  check(content, filePath) {
2250
- if (!isServerSideFile(filePath)) return [];
2354
+ if (!isServerSideFile(filePath, content)) return [];
2251
2355
  const matches = [];
2252
2356
  const patterns = [
2253
2357
  // Sending stack trace in response
@@ -2345,7 +2449,7 @@ var ssrfVulnerability = {
2345
2449
  category: "Injection",
2346
2450
  description: "Fetching URLs from user input without validation allows attackers to access internal services, cloud metadata endpoints (169.254.169.254), and private networks.",
2347
2451
  check(content, filePath) {
2348
- if (!isServerSideFile(filePath)) return [];
2452
+ if (!isServerSideFile(filePath, content)) return [];
2349
2453
  const matches = [];
2350
2454
  const hasValidation = /allowedHosts|allowedDomains|allowedUrls|safeDomain|whitelist|urlValidator|new URL.*hostname.*includes|isAllowedUrl|validateUrl|isValidUrl/i.test(content);
2351
2455
  if (hasValidation) return [];
@@ -2402,7 +2506,7 @@ var massAssignment = {
2402
2506
  category: "Authorization",
2403
2507
  description: "Spreading or assigning request body directly into database models allows attackers to set fields they shouldn't (e.g., isAdmin, role, verified).",
2404
2508
  check(content, filePath) {
2405
- if (!isServerSideFile(filePath)) return [];
2509
+ if (!isServerSideFile(filePath, content)) return [];
2406
2510
  const hasSanitization = /pick\(|omit\(|allowedFields|sanitize|whitelist|permit|strong_params/i.test(content);
2407
2511
  if (hasSanitization) return [];
2408
2512
  const matches = [];
@@ -2554,7 +2658,7 @@ var logInjection = {
2554
2658
  category: "Injection",
2555
2659
  description: "Logging unsanitized user input allows attackers to forge log entries, inject malicious content, or exploit log aggregation systems via newlines and special characters.",
2556
2660
  check(content, filePath) {
2557
- if (!isServerSideFile(filePath)) return [];
2661
+ if (!isServerSideFile(filePath, content)) return [];
2558
2662
  const hasSanitization = /replace\s*\(\s*\/\[?\\r\\n\]|sanitizeLog|stripNewlines|sanitizeForLog/i.test(content);
2559
2663
  if (hasSanitization) return [];
2560
2664
  const matches = [];
@@ -2620,7 +2724,7 @@ var weakPasswordRequirements = {
2620
2724
  if (!/(?:password|passwd|pwd)/i.test(content)) return [];
2621
2725
  const isPasswordContext = /(?:register|signup|sign.up|createUser|create.user|changePassword|resetPassword|set.password|validatePassword|validate.password|passwordPolicy|password.policy)/i.test(
2622
2726
  content
2623
- ) || isServerSideFile(filePath);
2727
+ ) || isServerSideFile(filePath, content);
2624
2728
  const matches = [];
2625
2729
  if (isPasswordContext) {
2626
2730
  const weakThresholdPatterns = [
@@ -3774,8 +3878,8 @@ var xxeVulnerability = {
3774
3878
  if (!/xml|parseXml|parseXML|DOMParser|SAXParser|etree|lxml|libxml/i.test(content)) return [];
3775
3879
  const matches = [];
3776
3880
  const patterns = [
3777
- /\.parseXm?l\s*\(/gi,
3778
- // catches parseXml (libxmljs) AND parseXML
3881
+ /\.parseXm?l(?:String)?\s*\(/gi,
3882
+ // parseXml / parseXML / parseXmlString (libxmljs)
3779
3883
  // NOTE: the browser `new DOMParser()` is intentionally NOT flagged.
3780
3884
  // Per the HTML/XML spec, DOMParser.parseFromString does not resolve
3781
3885
  // external entities, so it is not an XXE sink — flagging it produced a
@@ -3961,7 +4065,7 @@ var exposedAdminRoutes = {
3961
4065
  category: "Information Leakage",
3962
4066
  description: "Routes like /admin, /debug, /phpinfo, or /actuator without authentication expose sensitive controls and information to attackers.",
3963
4067
  check(content, filePath) {
3964
- if (!isServerSideFile(filePath)) return [];
4068
+ if (!isServerSideFile(filePath, content)) return [];
3965
4069
  const matches = [];
3966
4070
  const patterns = [
3967
4071
  /[.'"]\s*(?:get|use|all)\s*\(\s*["'`]\/(?:admin|debug|_debug|__debug__|phpinfo|actuator|graphiql|playground|swagger|reset|seed|test|dev|mock)["'`]/gi
@@ -4053,7 +4157,7 @@ var missingContentDisposition = {
4053
4157
  description: "File download endpoints without Content-Disposition headers may render files inline, leading to XSS if the file contains HTML/JS.",
4054
4158
  check(content, filePath) {
4055
4159
  if (!/(?:download|sendFile|send_file|pipe|createReadStream)/i.test(content)) return [];
4056
- if (!isServerSideFile(filePath)) return [];
4160
+ if (!isServerSideFile(filePath, content)) return [];
4057
4161
  if (/Content-Disposition|attachment|download/i.test(content)) return [];
4058
4162
  return findMatches(
4059
4163
  content,
@@ -4150,7 +4254,7 @@ var unprotectedDownload = {
4150
4254
  description: "File download endpoints that accept user-controlled filenames without path validation allow directory traversal to read arbitrary files.",
4151
4255
  check(content, filePath) {
4152
4256
  if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
4153
- if (!isServerSideFile(filePath)) return [];
4257
+ if (!isServerSideFile(filePath, content)) return [];
4154
4258
  const matches = [];
4155
4259
  if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4156
4260
  const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
@@ -4179,7 +4283,7 @@ var commandInjection = {
4179
4283
  const matches = [];
4180
4284
  const patterns = [
4181
4285
  // Node.js — require standalone exec/execSync, not db.exec() or conn.exec()
4182
- /(?<![.\w])(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
4286
+ /(?<![.\w])(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*["']\s*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
4183
4287
  /child_process.*exec\s*\(\s*(?!["'`])/g,
4184
4288
  // spawn / execFile / exec with shell: true AND a template literal
4185
4289
  // first arg. `shell: true` converts the first argument into a string