xploitscan-shared-rules 1.8.1 → 1.10.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
@@ -329,8 +329,140 @@ var TAINTED_REQUEST_OBJECTS = /* @__PURE__ */ new Set([
329
329
  "event"
330
330
  // Lambda
331
331
  ]);
332
+ var FS_READ_METHODS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
333
+ var PARSE_OBJECTS = /* @__PURE__ */ new Set([
334
+ "JSON",
335
+ "csv",
336
+ "papa",
337
+ "Papa",
338
+ "yaml",
339
+ "YAML",
340
+ "toml",
341
+ "qs",
342
+ "querystring"
343
+ ]);
344
+ var PARSE_BARE = /* @__PURE__ */ new Set(["parse", "parseSync"]);
345
+ var ARRAY_ELEMENT_METHODS = /* @__PURE__ */ new Set([
346
+ "map",
347
+ "filter",
348
+ "forEach",
349
+ "find",
350
+ "findIndex",
351
+ "flatMap",
352
+ "some",
353
+ "every"
354
+ ]);
355
+ var ARRAY_REDUCE_METHODS = /* @__PURE__ */ new Set(["reduce", "reduceRight"]);
356
+ function callPreservesTaint(node, rec) {
357
+ const callee = node.callee;
358
+ const anyArgTainted = () => node.arguments.some((a) => a.type !== "SpreadElement" && rec(a));
359
+ if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
360
+ const pname = callee.property.name;
361
+ if (FS_READ_METHODS.has(pname)) return anyArgTainted();
362
+ if (pname === "parse" || pname === "parseSync") {
363
+ const obj = callee.object;
364
+ if (obj.type === "Identifier" && PARSE_OBJECTS.has(obj.name)) return anyArgTainted();
365
+ }
366
+ }
367
+ if (callee.type === "Identifier") {
368
+ if (FS_READ_METHODS.has(callee.name)) return anyArgTainted();
369
+ if (PARSE_BARE.has(callee.name)) return anyArgTainted();
370
+ }
371
+ return false;
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
+ }
332
444
  function buildTaintMap(parsed) {
333
445
  const tainted = /* @__PURE__ */ new Set();
446
+ function markPatternTainted(target) {
447
+ if (target.type === "Identifier") {
448
+ tainted.add(target.name);
449
+ } else if (target.type === "ObjectPattern") {
450
+ for (const prop of target.properties) {
451
+ if (prop.type === "ObjectProperty" && prop.value.type === "Identifier") {
452
+ tainted.add(prop.value.name);
453
+ } else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
454
+ tainted.add(prop.argument.name);
455
+ }
456
+ }
457
+ } else if (target.type === "ArrayPattern") {
458
+ for (const el of target.elements) {
459
+ if (el && el.type === "Identifier") tainted.add(el.name);
460
+ else if (el && el.type === "RestElement" && el.argument.type === "Identifier") {
461
+ tainted.add(el.argument.name);
462
+ }
463
+ }
464
+ }
465
+ }
334
466
  function reachesRequestIdent(node) {
335
467
  if (!node) return false;
336
468
  if (node.type === "Identifier") return TAINTED_REQUEST_OBJECTS.has(node.name);
@@ -401,6 +533,8 @@ function buildTaintMap(parsed) {
401
533
  }
402
534
  if (node.type === "CallExpression") {
403
535
  if (nodeIsTaintedSource(node)) return true;
536
+ if (callPreservesTaint(node, exprIsTainted)) return true;
537
+ if (localCallPropagates(node, exprIsTainted)) return true;
404
538
  if (node.callee.type === "MemberExpression") {
405
539
  if (exprIsTainted(node.callee.object)) return true;
406
540
  const obj = node.callee.object;
@@ -421,8 +555,46 @@ function buildTaintMap(parsed) {
421
555
  if (node.type === "AwaitExpression") {
422
556
  return exprIsTainted(node.argument);
423
557
  }
558
+ if (node.type === "ArrayExpression") {
559
+ return node.elements.some(
560
+ (el) => el != null && el.type === "SpreadElement" && exprIsTainted(el.argument)
561
+ );
562
+ }
563
+ if (node.type === "ObjectExpression") {
564
+ return node.properties.some(
565
+ (p) => p.type === "SpreadElement" && exprIsTainted(p.argument)
566
+ );
567
+ }
424
568
  return false;
425
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
+ }
426
598
  traverse(parsed.ast, {
427
599
  VariableDeclarator(path) {
428
600
  const node = path.node;
@@ -455,6 +627,32 @@ function buildTaintMap(parsed) {
455
627
  if (exprIsTainted(node.right)) {
456
628
  tainted.add(node.left.name);
457
629
  }
630
+ },
631
+ // for (const row of <tainted>) → row tainted (and destructured names).
632
+ ForOfStatement(path) {
633
+ const node = path.node;
634
+ if (node.type !== "ForOfStatement") return;
635
+ if (!exprIsTainted(node.right)) return;
636
+ const decl = node.left;
637
+ const target = decl.type === "VariableDeclaration" && decl.declarations[0] ? decl.declarations[0].id : decl;
638
+ markPatternTainted(target);
639
+ },
640
+ // Array-iteration element taint: <tainted>.map((row) => …) marks `row`
641
+ // tainted inside the callback. Gated on the receiver already being
642
+ // tainted, so this never originates taint.
643
+ CallExpression(path) {
644
+ const node = path.node;
645
+ if (node.type !== "CallExpression") return;
646
+ const callee = node.callee;
647
+ if (callee.type !== "MemberExpression" || callee.property.type !== "Identifier") return;
648
+ const method = callee.property.name;
649
+ const isReduce = ARRAY_REDUCE_METHODS.has(method);
650
+ if (!ARRAY_ELEMENT_METHODS.has(method) && !isReduce) return;
651
+ if (!exprIsTainted(callee.object)) return;
652
+ const cb = node.arguments[0];
653
+ if (!cb || cb.type !== "ArrowFunctionExpression" && cb.type !== "FunctionExpression") return;
654
+ const elemParam = cb.params[isReduce ? 1 : 0];
655
+ if (elemParam) markPatternTainted(elemParam);
458
656
  }
459
657
  });
460
658
  const isTainted = (node) => {
@@ -480,6 +678,8 @@ function buildTaintMap(parsed) {
480
678
  return isTainted(node.object);
481
679
  }
482
680
  if (node.type === "CallExpression") {
681
+ if (callPreservesTaint(node, isTainted)) return true;
682
+ if (localCallPropagates(node, isTainted)) return true;
483
683
  if (node.callee.type === "MemberExpression") {
484
684
  if (isTainted(node.callee.object)) return true;
485
685
  const obj = node.callee.object;
@@ -497,6 +697,16 @@ function buildTaintMap(parsed) {
497
697
  }
498
698
  }
499
699
  }
700
+ if (node.type === "ArrayExpression") {
701
+ return node.elements.some(
702
+ (el) => el != null && el.type === "SpreadElement" && isTainted(el.argument)
703
+ );
704
+ }
705
+ if (node.type === "ObjectExpression") {
706
+ return node.properties.some(
707
+ (p) => p.type === "SpreadElement" && isTainted(p.argument)
708
+ );
709
+ }
500
710
  return false;
501
711
  };
502
712
  return {