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 +118 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +118 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
|
4043
|
-
//
|
|
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
|