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/README.md +1 -1
- package/dist/index.cjs +210 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +210 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 {
|