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.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;
|
|
@@ -1217,6 +1217,41 @@ var sqlInjection = {
|
|
|
1217
1217
|
matches.push(m);
|
|
1218
1218
|
}
|
|
1219
1219
|
}
|
|
1220
|
+
if (/\.\s*(?:query|execute|raw|queryRawUnsafe|executeRawUnsafe|\$queryRawUnsafe|\$executeRawUnsafe)\s*\(/.test(content)) {
|
|
1221
|
+
const ctx = tryParse(content, filePath);
|
|
1222
|
+
if (ctx) {
|
|
1223
|
+
const { parsed, taint } = ctx;
|
|
1224
|
+
const SQL_SINKS = /* @__PURE__ */ new Set([
|
|
1225
|
+
"query",
|
|
1226
|
+
"execute",
|
|
1227
|
+
"raw",
|
|
1228
|
+
"queryRawUnsafe",
|
|
1229
|
+
"executeRawUnsafe",
|
|
1230
|
+
"$queryRawUnsafe",
|
|
1231
|
+
"$executeRawUnsafe"
|
|
1232
|
+
]);
|
|
1233
|
+
visitCalls(
|
|
1234
|
+
parsed,
|
|
1235
|
+
(callee) => callee.type === "MemberExpression" && callee.property.type === "Identifier" && SQL_SINKS.has(callee.property.name),
|
|
1236
|
+
(call, line) => {
|
|
1237
|
+
const first = call.arguments[0];
|
|
1238
|
+
if (!first || first.type === "SpreadElement") return;
|
|
1239
|
+
if (first.type === "StringLiteral" || first.type === "TemplateLiteral") return;
|
|
1240
|
+
if (!taint.isTainted(first)) return;
|
|
1241
|
+
if (matches.some((m) => m.line === line)) return;
|
|
1242
|
+
matches.push(
|
|
1243
|
+
astMatch(
|
|
1244
|
+
content,
|
|
1245
|
+
filePath,
|
|
1246
|
+
line,
|
|
1247
|
+
sqlInjection,
|
|
1248
|
+
"Use parameterized queries instead of building SQL from user input in a variable. Example: db.query('SELECT * FROM users WHERE login = ?', [req.body.login])"
|
|
1249
|
+
)
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1220
1255
|
return matches;
|
|
1221
1256
|
}
|
|
1222
1257
|
};
|
|
@@ -1227,9 +1262,10 @@ var xssVulnerability = {
|
|
|
1227
1262
|
category: "Injection",
|
|
1228
1263
|
description: "Rendering user input without sanitization allows attackers to inject malicious scripts.",
|
|
1229
1264
|
check(content, filePath) {
|
|
1265
|
+
const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
|
|
1230
1266
|
if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
|
|
1231
|
-
if (/DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
1232
|
-
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 [];
|
|
1233
1269
|
const patterns = [
|
|
1234
1270
|
// React dangerouslySetInnerHTML
|
|
1235
1271
|
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
@@ -1240,7 +1276,12 @@ var xssVulnerability = {
|
|
|
1240
1276
|
// v-html in Vue
|
|
1241
1277
|
/v-html\s*=/g,
|
|
1242
1278
|
// {@html} in Svelte
|
|
1243
|
-
/\{@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
|
|
1244
1285
|
];
|
|
1245
1286
|
const allLines = content.split("\n");
|
|
1246
1287
|
const matches = [];
|
|
@@ -2461,12 +2502,12 @@ var ssrfVulnerability = {
|
|
|
2461
2502
|
filePath,
|
|
2462
2503
|
() => "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');"
|
|
2463
2504
|
));
|
|
2464
|
-
if (!/\b(?:fetch|axios|got|request|http\.get|https\.get)\b/.test(content)) return matches;
|
|
2505
|
+
if (!/\b(?:fetch|axios|got|request|needle|superagent|undici|http\.get|https\.get)\b/.test(content)) return matches;
|
|
2465
2506
|
const ctx = tryParse(content, filePath);
|
|
2466
2507
|
if (!ctx) return matches;
|
|
2467
2508
|
const { parsed, taint } = ctx;
|
|
2468
|
-
const FETCH_CALLEES = /* @__PURE__ */ new Set(["fetch", "axios", "got", "request"]);
|
|
2469
|
-
const FETCH_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "request"]);
|
|
2509
|
+
const FETCH_CALLEES = /* @__PURE__ */ new Set(["fetch", "axios", "got", "request", "needle", "superagent", "undici"]);
|
|
2510
|
+
const FETCH_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "request", "head"]);
|
|
2470
2511
|
visitCalls(
|
|
2471
2512
|
parsed,
|
|
2472
2513
|
(callee) => {
|
|
@@ -2475,7 +2516,7 @@ var ssrfVulnerability = {
|
|
|
2475
2516
|
if (!FETCH_METHODS.has(callee.property.name)) return false;
|
|
2476
2517
|
const obj = callee.object;
|
|
2477
2518
|
if (obj.type === "Identifier") {
|
|
2478
|
-
return obj.name === "axios" || obj.name === "got" || obj.name === "http" || obj.name === "https";
|
|
2519
|
+
return obj.name === "axios" || obj.name === "got" || obj.name === "http" || obj.name === "https" || obj.name === "needle" || obj.name === "superagent" || obj.name === "undici";
|
|
2479
2520
|
}
|
|
2480
2521
|
}
|
|
2481
2522
|
return false;
|
|
@@ -3983,17 +4024,37 @@ var ssti = {
|
|
|
3983
4024
|
(call, line) => {
|
|
3984
4025
|
const first = call.arguments[0];
|
|
3985
4026
|
if (!first || first.type === "SpreadElement") return;
|
|
3986
|
-
if (
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
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
|
+
}
|
|
3997
4058
|
}
|
|
3998
4059
|
);
|
|
3999
4060
|
return filterSilenced(matches, content, "VC082");
|
|
@@ -4256,16 +4317,61 @@ var unprotectedDownload = {
|
|
|
4256
4317
|
if (!/(?:download|sendFile|send_file)/i.test(content)) return [];
|
|
4257
4318
|
if (!isServerSideFile(filePath, content)) return [];
|
|
4258
4319
|
const matches = [];
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
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
|
+
);
|
|
4269
4375
|
}
|
|
4270
4376
|
}
|
|
4271
4377
|
return matches;
|
|
@@ -5153,7 +5259,7 @@ var pathTraversal = {
|
|
|
5153
5259
|
while ((m = pat.exec(content)) !== null) {
|
|
5154
5260
|
if (isCommentLine(content, m.index)) continue;
|
|
5155
5261
|
const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
|
|
5156
|
-
if (/path\.normalize|sanitize|\.replace\(\s*['"]
|
|
5262
|
+
if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
|
|
5157
5263
|
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
5158
5264
|
findings.push({
|
|
5159
5265
|
rule: "VC117",
|