xploitscan-shared-rules 1.12.0 → 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 +97 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +97 -26
- 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;
|
|
@@ -1527,9 +1527,10 @@ var xssVulnerability = {
|
|
|
1527
1527
|
category: "Injection",
|
|
1528
1528
|
description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
|
|
1529
1529
|
check(content, filePath) {
|
|
1530
|
+
const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
|
|
1530
1531
|
if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
|
|
1531
|
-
if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
1532
|
-
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 [];
|
|
1533
1534
|
const patterns = [
|
|
1534
1535
|
// React dangerouslySetInnerHTML
|
|
1535
1536
|
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
@@ -1540,7 +1541,12 @@ var xssVulnerability = {
|
|
|
1540
1541
|
// v-html in Vue
|
|
1541
1542
|
/v-html\s*=/g,
|
|
1542
1543
|
// {@html} in Svelte
|
|
1543
|
-
/\{@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
|
|
1544
1550
|
];
|
|
1545
1551
|
const allLines = content.split("\n");
|
|
1546
1552
|
const matches = [];
|
|
@@ -4283,17 +4289,37 @@ var ssti = {
|
|
|
4283
4289
|
(call, line) => {
|
|
4284
4290
|
const first = call.arguments[0];
|
|
4285
4291
|
if (!first || first.type === "SpreadElement") return;
|
|
4286
|
-
if (
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
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
|
+
}
|
|
4297
4323
|
}
|
|
4298
4324
|
);
|
|
4299
4325
|
return filterSilenced(matches, content, "VC082");
|
|
@@ -4556,16 +4582,61 @@ var unprotectedDownload = {
|
|
|
4556
4582
|
if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
|
|
4557
4583
|
if (!isServerSideFile(filePath, content)) return [];
|
|
4558
4584
|
const matches = [];
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
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
|
+
);
|
|
4569
4640
|
}
|
|
4570
4641
|
}
|
|
4571
4642
|
return matches;
|
|
@@ -5453,7 +5524,7 @@ var pathTraversal = {
|
|
|
5453
5524
|
while ((m = pat.exec(content)) !== null) {
|
|
5454
5525
|
if (isCommentLine(content, m.index)) continue;
|
|
5455
5526
|
const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
|
|
5456
|
-
if (/path\.normalize|sanitize|\.replace\(\s*['"]
|
|
5527
|
+
if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
|
|
5457
5528
|
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
5458
5529
|
findings.push({
|
|
5459
5530
|
rule: "VC117",
|