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.js
CHANGED
|
@@ -815,7 +815,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
|
|
|
815
815
|
].join("|"),
|
|
816
816
|
"i"
|
|
817
817
|
);
|
|
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/;
|
|
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|(?:import|require)\b[^;\n]*['"]express['"]|\bNextFunction\b/;
|
|
819
819
|
function isServerSideFile(filePath, content) {
|
|
820
820
|
if (isTestFile(filePath)) return false;
|
|
821
821
|
if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
|
|
@@ -1262,9 +1262,10 @@ var xssVulnerability = {
|
|
|
1262
1262
|
category: "Injection",
|
|
1263
1263
|
description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
|
|
1264
1264
|
check(content, filePath) {
|
|
1265
|
+
const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
|
|
1265
1266
|
if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
|
|
1266
|
-
if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
1267
|
-
if (/(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
|
|
1267
|
+
if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
1268
|
+
if (!hasSanitizerBypass && /(?:import|require)\s*\(?.*(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
|
|
1268
1269
|
const patterns = [
|
|
1269
1270
|
// React dangerouslySetInnerHTML
|
|
1270
1271
|
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
@@ -1275,7 +1276,12 @@ var xssVulnerability = {
|
|
|
1275
1276
|
// v-html in Vue
|
|
1276
1277
|
/v-html\s*=/g,
|
|
1277
1278
|
// {@html} in Svelte
|
|
1278
|
-
/\{@html\s/g
|
|
1279
|
+
/\{@html\s/g,
|
|
1280
|
+
// Angular DomSanitizer escape hatches — bypassSecurityTrustHtml /
|
|
1281
|
+
// bypassSecurityTrustScript / ...Url / ...ResourceUrl / ...Style. These
|
|
1282
|
+
// mark a value as trusted and skip Angular's built-in sanitization;
|
|
1283
|
+
// passing user input through one is the canonical Angular DOM-XSS sink.
|
|
1284
|
+
/bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/g
|
|
1279
1285
|
];
|
|
1280
1286
|
const allLines = content.split("\n");
|
|
1281
1287
|
const matches = [];
|
|
@@ -4018,17 +4024,37 @@ var ssti = {
|
|
|
4018
4024
|
(call, line) => {
|
|
4019
4025
|
const first = call.arguments[0];
|
|
4020
4026
|
if (!first || first.type === "SpreadElement") return;
|
|
4021
|
-
if (
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4027
|
+
if (taint.isTainted(first)) {
|
|
4028
|
+
if (matches.some((m) => m.line === line)) return;
|
|
4029
|
+
matches.push(
|
|
4030
|
+
astMatch(
|
|
4031
|
+
content,
|
|
4032
|
+
filePath,
|
|
4033
|
+
line,
|
|
4034
|
+
ssti,
|
|
4035
|
+
"Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
|
|
4036
|
+
)
|
|
4037
|
+
);
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
4040
|
+
const opts = call.arguments[1];
|
|
4041
|
+
if (opts && opts.type === "ObjectExpression") {
|
|
4042
|
+
const hasTaintedSpread = opts.properties.some(
|
|
4043
|
+
(p) => p.type === "SpreadElement" && taint.isTainted(p.argument)
|
|
4044
|
+
);
|
|
4045
|
+
if (hasTaintedSpread) {
|
|
4046
|
+
if (matches.some((m) => m.line === line)) return;
|
|
4047
|
+
matches.push(
|
|
4048
|
+
astMatch(
|
|
4049
|
+
content,
|
|
4050
|
+
filePath,
|
|
4051
|
+
line,
|
|
4052
|
+
ssti,
|
|
4053
|
+
"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 })."
|
|
4054
|
+
)
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4032
4058
|
}
|
|
4033
4059
|
);
|
|
4034
4060
|
return filterSilenced(matches, content, "VC082");
|
|
@@ -4291,16 +4317,61 @@ var unprotectedDownload = {
|
|
|
4291
4317
|
if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
|
|
4292
4318
|
if (!isServerSideFile(filePath, content)) return [];
|
|
4293
4319
|
const matches = [];
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4320
|
+
const hasContainmentCheck = /\.startsWith\s*\(/.test(content) || /realpath/i.test(content) || /includes\s*\(\s*["']\.\./.test(content);
|
|
4321
|
+
if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content) && !hasContainmentCheck) {
|
|
4322
|
+
matches.push(...findMatches(
|
|
4323
|
+
content,
|
|
4324
|
+
/(?:sendFile|download|send_file)\s*\(/gi,
|
|
4325
|
+
unprotectedDownload,
|
|
4326
|
+
filePath,
|
|
4327
|
+
() => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
|
|
4328
|
+
));
|
|
4329
|
+
}
|
|
4330
|
+
if (!hasContainmentCheck && /\b(?:sendFile|download)\s*\(/.test(content)) {
|
|
4331
|
+
const ctx = tryParse(content, filePath);
|
|
4332
|
+
if (ctx) {
|
|
4333
|
+
const { parsed, taint } = ctx;
|
|
4334
|
+
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);
|
|
4335
|
+
const isUnsafePathBuild = (node) => {
|
|
4336
|
+
if (node.type !== "CallExpression") return false;
|
|
4337
|
+
const callee = node.callee;
|
|
4338
|
+
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");
|
|
4339
|
+
if (!isPathBuild) return false;
|
|
4340
|
+
const rootedAtDirname = node.arguments.some(
|
|
4341
|
+
(a) => a.type === "Identifier" && (a.name === "__dirname" || a.name === "__filename")
|
|
4342
|
+
);
|
|
4343
|
+
if (rootedAtDirname) return false;
|
|
4344
|
+
return node.arguments.some(
|
|
4345
|
+
(a) => a.type !== "SpreadElement" && a.type !== "StringLiteral" && a.type !== "TemplateLiteral"
|
|
4346
|
+
);
|
|
4347
|
+
};
|
|
4348
|
+
const argIsTainted = (node) => {
|
|
4349
|
+
if (taint.isTainted(node)) return true;
|
|
4350
|
+
if (readsUserInput && isUnsafePathBuild(node)) return true;
|
|
4351
|
+
if (node.type === "CallExpression") {
|
|
4352
|
+
return node.arguments.some((a) => a.type !== "SpreadElement" && argIsTainted(a));
|
|
4353
|
+
}
|
|
4354
|
+
return false;
|
|
4355
|
+
};
|
|
4356
|
+
visitCalls(
|
|
4357
|
+
parsed,
|
|
4358
|
+
(callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && (callee.property.name === "sendFile" || callee.property.name === "download"),
|
|
4359
|
+
(call, line) => {
|
|
4360
|
+
const first = call.arguments[0];
|
|
4361
|
+
if (!first || first.type === "SpreadElement") return;
|
|
4362
|
+
if (!argIsTainted(first)) return;
|
|
4363
|
+
if (matches.some((m) => m.line === line)) return;
|
|
4364
|
+
matches.push(
|
|
4365
|
+
astMatch(
|
|
4366
|
+
content,
|
|
4367
|
+
filePath,
|
|
4368
|
+
line,
|
|
4369
|
+
unprotectedDownload,
|
|
4370
|
+
"Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR + path.sep)) throw new Error('Invalid path');"
|
|
4371
|
+
)
|
|
4372
|
+
);
|
|
4373
|
+
}
|
|
4374
|
+
);
|
|
4304
4375
|
}
|
|
4305
4376
|
}
|
|
4306
4377
|
return matches;
|
|
@@ -5188,7 +5259,7 @@ var pathTraversal = {
|
|
|
5188
5259
|
while ((m = pat.exec(content)) !== null) {
|
|
5189
5260
|
if (isCommentLine(content, m.index)) continue;
|
|
5190
5261
|
const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
|
|
5191
|
-
if (/path\.normalize|sanitize|\.replace\(\s*['"]
|
|
5262
|
+
if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
|
|
5192
5263
|
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
5193
5264
|
findings.push({
|
|
5194
5265
|
rule: "VC117",
|