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.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 (!taint.isTainted(first)) return;
4022
- if (matches.some((m) => m.line === line)) return;
4023
- matches.push(
4024
- astMatch(
4025
- content,
4026
- filePath,
4027
- line,
4028
- ssti,
4029
- "Compile templates from a trusted, static source (a file path or a constant string). Pass user data only as context values."
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
- if (/(?:sendFile|download|send_file)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/i.test(content)) {
4295
- const hasValidation = /path\.resolve|path\.normalize|path\.join.*__dirname|realpath|includes\s*\(\s*["']\.\./i.test(content);
4296
- if (!hasValidation) {
4297
- matches.push(...findMatches(
4298
- content,
4299
- /(?:sendFile|download|send_file)\s*\(/gi,
4300
- unprotectedDownload,
4301
- filePath,
4302
- () => "Validate file paths: const safePath = path.resolve(DOWNLOAD_DIR, filename); if (!safePath.startsWith(DOWNLOAD_DIR)) throw new Error('Invalid path');"
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*['"]\.\./i.test(surrounding)) continue;
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",