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.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
|
-
|
|
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
|
-
|
|
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
|
|
3778
|
-
//
|
|
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
|