xploitscan-shared-rules 1.11.1 → 1.13.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 +136 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +136 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1080,7 +1080,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
|
|
|
1080
1080
|
].join("|"),
|
|
1081
1081
|
"i"
|
|
1082
1082
|
);
|
|
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/;
|
|
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|(?:import|require)\b[^;\n]*['"]express['"]|\bNextFunction\b/;
|
|
1084
1084
|
function isServerSideFile(filePath, content) {
|
|
1085
1085
|
if (isTestFile(filePath)) return false;
|
|
1086
1086
|
if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
|
|
@@ -1482,6 +1482,41 @@ var sqlInjection = {
|
|
|
1482
1482
|
matches.push(m);
|
|
1483
1483
|
}
|
|
1484
1484
|
}
|
|
1485
|
+
if (/\.\s*(?:query|execute|raw|queryRawUnsafe|executeRawUnsafe|\$queryRawUnsafe|\$executeRawUnsafe)\s*\(/.test(content)) {
|
|
1486
|
+
const ctx = tryParse(content, filePath);
|
|
1487
|
+
if (ctx) {
|
|
1488
|
+
const { parsed, taint } = ctx;
|
|
1489
|
+
const SQL_SINKS = /* @__PURE__ */ new Set([
|
|
1490
|
+
"query",
|
|
1491
|
+
"execute",
|
|
1492
|
+
"raw",
|
|
1493
|
+
"queryRawUnsafe",
|
|
1494
|
+
"executeRawUnsafe",
|
|
1495
|
+
"$queryRawUnsafe",
|
|
1496
|
+
"$executeRawUnsafe"
|
|
1497
|
+
]);
|
|
1498
|
+
visitCalls(
|
|
1499
|
+
parsed,
|
|
1500
|
+
(callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && SQL_SINKS.has(callee.property.name),
|
|
1501
|
+
(call, line) => {
|
|
1502
|
+
const first = call.arguments[0];
|
|
1503
|
+
if (!first || first.type === "SpreadElement") return;
|
|
1504
|
+
if (first.type === "StringLiteral" || first.type === "TemplateLiteral") return;
|
|
1505
|
+
if (!taint.isTainted(first)) return;
|
|
1506
|
+
if (matches.some((m) => m.line === line)) return;
|
|
1507
|
+
matches.push(
|
|
1508
|
+
astMatch(
|
|
1509
|
+
content,
|
|
1510
|
+
filePath,
|
|
1511
|
+
line,
|
|
1512
|
+
sqlInjection,
|
|
1513
|
+
"Use parameterized queries instead of building SQL from user input in a variable. Example: db.query('SELECT * FROM users WHERE login = ?', [req.body.login])"
|
|
1514
|
+
)
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1485
1520
|
return matches;
|
|
1486
1521
|
}
|
|
1487
1522
|
};
|
|
@@ -1492,9 +1527,10 @@ var xssVulnerability = {
|
|
|
1492
1527
|
category: "Injection",
|
|
1493
1528
|
description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
|
|
1494
1529
|
check(content, filePath) {
|
|
1530
|
+
const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
|
|
1495
1531
|
if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
|
|
1496
|
-
if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
1497
|
-
if (/(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
|
|
1532
|
+
if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
1533
|
+
if (!hasSanitizerBypass && /(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
|
|
1498
1534
|
const patterns = [
|
|
1499
1535
|
// React dangerouslySetInnerHTML
|
|
1500
1536
|
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
@@ -1505,7 +1541,12 @@ var xssVulnerability = {
|
|
|
1505
1541
|
// v-html in Vue
|
|
1506
1542
|
/v-html\s*=/g,
|
|
1507
1543
|
// {@html} in Svelte
|
|
1508
|
-
/\{@html\s/g
|
|
1544
|
+
/\{@html\s/g,
|
|
1545
|
+
// Angular DomSanitizer escape hatches — bypassSecurityTrustHtml /
|
|
1546
|
+
// bypassSecurityTrustScript / ...Url / ...ResourceUrl / ...Style. These
|
|
1547
|
+
// mark a value as trusted and skip Angular's built-in sanitization;
|
|
1548
|
+
// passing user input through one is the canonical Angular DOM-XSS sink.
|
|
1549
|
+
/bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/g
|
|
1509
1550
|
];
|
|
1510
1551
|
const allLines = content.split("\n");
|
|
1511
1552
|
const matches = [];
|
|
@@ -2726,12 +2767,12 @@ var ssrfVulnerability = {
|
|
|
2726
2767
|
filePath,
|
|
2727
2768
|
() => "Validate URLs against an allowlist before fetching. Block internal IPs: 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254 (cloud metadata). Use: const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');"
|
|
2728
2769
|
));
|
|
2729
|
-
if (!/\b(?:fetch|axios|got|request|http\.get|https\.get)\b/.test(content)) return matches;
|
|
2770
|
+
if (!/\b(?:fetch|axios|got|request|needle|superagent|undici|http\.get|https\.get)\b/.test(content)) return matches;
|
|
2730
2771
|
const ctx = tryParse(content, filePath);
|
|
2731
2772
|
if (!ctx) return matches;
|
|
2732
2773
|
const { parsed, taint } = ctx;
|
|
2733
|
-
const FETCH_CALLEES = /* @__PURE__ */ new Set(["fetch", "axios", "got", "request"]);
|
|
2734
|
-
const FETCH_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "request"]);
|
|
2774
|
+
const FETCH_CALLEES = /* @__PURE__ */ new Set(["fetch", "axios", "got", "request", "needle", "superagent", "undici"]);
|
|
2775
|
+
const FETCH_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "request", "head"]);
|
|
2735
2776
|
visitCalls(
|
|
2736
2777
|
parsed,
|
|
2737
2778
|
(callee) => {
|
|
@@ -2740,7 +2781,7 @@ var ssrfVulnerability = {
|
|
|
2740
2781
|
if (!FETCH_METHODS.has(callee.property.name)) return false;
|
|
2741
2782
|
const obj = callee.object;
|
|
2742
2783
|
if (obj.type === "Identifier") {
|
|
2743
|
-
return obj.name === "axios" || obj.name === "got" || obj.name === "http" || obj.name === "https";
|
|
2784
|
+
return obj.name === "axios" || obj.name === "got" || obj.name === "http" || obj.name === "https" || obj.name === "needle" || obj.name === "superagent" || obj.name === "undici";
|
|
2744
2785
|
}
|
|
2745
2786
|
}
|
|
2746
2787
|
return false;
|
|
@@ -4248,17 +4289,37 @@ var ssti = {
|
|
|
4248
4289
|
(call, line) => {
|
|
4249
4290
|
const first = call.arguments[0];
|
|
4250
4291
|
if (!first || first.type === "SpreadElement") return;
|
|
4251
|
-
if (
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4292
|
+
if (taint.isTainted(first)) {
|
|
4293
|
+
if (matches.some((m) => m.line === line)) return;
|
|
4294
|
+
matches.push(
|
|
4295
|
+
astMatch(
|
|
4296
|
+
content,
|
|
4297
|
+
filePath,
|
|
4298
|
+
line,
|
|
4299
|
+
ssti,
|
|
4300
|
+
"Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
|
|
4301
|
+
)
|
|
4302
|
+
);
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
const opts = call.arguments[1];
|
|
4306
|
+
if (opts && opts.type === "ObjectExpression") {
|
|
4307
|
+
const hasTaintedSpread = opts.properties.some(
|
|
4308
|
+
(p) => p.type === "SpreadElement" && taint.isTainted(p.argument)
|
|
4309
|
+
);
|
|
4310
|
+
if (hasTaintedSpread) {
|
|
4311
|
+
if (matches.some((m) => m.line === line)) return;
|
|
4312
|
+
matches.push(
|
|
4313
|
+
astMatch(
|
|
4314
|
+
content,
|
|
4315
|
+
filePath,
|
|
4316
|
+
line,
|
|
4317
|
+
ssti,
|
|
4318
|
+
"Don't spread req.body/req.query into template locals \u2014 an attacker can inject reserved view options (e.g. `layout`) to load arbitrary templates. Pass only the specific values the view needs: render('view', { name: req.body.name })."
|
|
4319
|
+
)
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4262
4323
|
}
|
|
4263
4324
|
);
|
|
4264
4325
|
return filterSilenced(matches, content, "VC082");
|
|
@@ -4521,16 +4582,61 @@ var unprotectedDownload = {
|
|
|
4521
4582
|
if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
|
|
4522
4583
|
if (!isServerSideFile(filePath, content)) return [];
|
|
4523
4584
|
const matches = [];
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4585
|
+
const hasContainmentCheck = /\.startsWith\s*\(/.test(content) || /realpath/i.test(content) || /includes\s*\(\s*["']\.\./.test(content);
|
|
4586
|
+
if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content) && !hasContainmentCheck) {
|
|
4587
|
+
matches.push(...findMatches(
|
|
4588
|
+
content,
|
|
4589
|
+
/(?:sendFile|download|send_file)\s*\(/gi,
|
|
4590
|
+
unprotectedDownload,
|
|
4591
|
+
filePath,
|
|
4592
|
+
() => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
|
|
4593
|
+
));
|
|
4594
|
+
}
|
|
4595
|
+
if (!hasContainmentCheck && /\b(?:sendFile|download)\s*\(/.test(content)) {
|
|
4596
|
+
const ctx = tryParse(content, filePath);
|
|
4597
|
+
if (ctx) {
|
|
4598
|
+
const { parsed, taint } = ctx;
|
|
4599
|
+
const readsUserInput = /\b(?:req|request)\s*\.\s*(?:params|query|body|headers|cookies)\b/.test(content) || /\bparams\s*\.\s*\w/.test(content) || /\bquery\s*\.\s*\w/.test(content) || /\breq\.body\b/.test(content);
|
|
4600
|
+
const isUnsafePathBuild = (node) => {
|
|
4601
|
+
if (node.type !== "CallExpression") return false;
|
|
4602
|
+
const callee = node.callee;
|
|
4603
|
+
const isPathBuild = callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && (callee.property.name === "resolve" || callee.property.name === "join");
|
|
4604
|
+
if (!isPathBuild) return false;
|
|
4605
|
+
const rootedAtDirname = node.arguments.some(
|
|
4606
|
+
(a) => a.type === "Identifier" && (a.name === "__dirname" || a.name === "__filename")
|
|
4607
|
+
);
|
|
4608
|
+
if (rootedAtDirname) return false;
|
|
4609
|
+
return node.arguments.some(
|
|
4610
|
+
(a) => a.type !== "SpreadElement" && a.type !== "StringLiteral" && a.type !== "TemplateLiteral"
|
|
4611
|
+
);
|
|
4612
|
+
};
|
|
4613
|
+
const argIsTainted = (node) => {
|
|
4614
|
+
if (taint.isTainted(node)) return true;
|
|
4615
|
+
if (readsUserInput && isUnsafePathBuild(node)) return true;
|
|
4616
|
+
if (node.type === "CallExpression") {
|
|
4617
|
+
return node.arguments.some((a) => a.type !== "SpreadElement" && argIsTainted(a));
|
|
4618
|
+
}
|
|
4619
|
+
return false;
|
|
4620
|
+
};
|
|
4621
|
+
visitCalls(
|
|
4622
|
+
parsed,
|
|
4623
|
+
(callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && (callee.property.name === "sendFile" || callee.property.name === "download"),
|
|
4624
|
+
(call, line) => {
|
|
4625
|
+
const first = call.arguments[0];
|
|
4626
|
+
if (!first || first.type === "SpreadElement") return;
|
|
4627
|
+
if (!argIsTainted(first)) return;
|
|
4628
|
+
if (matches.some((m) => m.line === line)) return;
|
|
4629
|
+
matches.push(
|
|
4630
|
+
astMatch(
|
|
4631
|
+
content,
|
|
4632
|
+
filePath,
|
|
4633
|
+
line,
|
|
4634
|
+
unprotectedDownload,
|
|
4635
|
+
"Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
|
|
4636
|
+
)
|
|
4637
|
+
);
|
|
4638
|
+
}
|
|
4639
|
+
);
|
|
4534
4640
|
}
|
|
4535
4641
|
}
|
|
4536
4642
|
return matches;
|
|
@@ -5418,7 +5524,7 @@ var pathTraversal = {
|
|
|
5418
5524
|
while ((m = pat.exec(content)) !== null) {
|
|
5419
5525
|
if (isCommentLine(content, m.index)) continue;
|
|
5420
5526
|
const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
|
|
5421
|
-
if (/path\.normalize|sanitize|\.replace\(\s*['"]
|
|
5527
|
+
if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
|
|
5422
5528
|
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
5423
5529
|
findings.push({
|
|
5424
5530
|
rule: "VC117",
|