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.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 (!taint.isTainted(first)) return;
3987
- if (matches.some((m) => m.line === line)) return;
3988
- matches.push(
3989
- astMatch(
3990
- content,
3991
- filePath,
3992
- line,
3993
- ssti,
3994
- "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
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
- if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4260
- const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
4261
- if (!hasValidation) {
4262
- matches.push(...findMatches(
4263
- content,
4264
- /(?:sendFile|download|send_file)\s*\(/gi,
4265
- unprotectedDownload,
4266
- filePath,
4267
- () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
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*['"]\.\./i.test(surrounding)) continue;
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",