circle-ir 3.140.0 → 3.144.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.
Files changed (41) hide show
  1. package/configs/sink-semantics.json +53 -0
  2. package/dist/analysis/config-loader.d.ts +24 -2
  3. package/dist/analysis/config-loader.d.ts.map +1 -1
  4. package/dist/analysis/config-loader.js +92 -2
  5. package/dist/analysis/config-loader.js.map +1 -1
  6. package/dist/analysis/findings.d.ts +32 -0
  7. package/dist/analysis/findings.d.ts.map +1 -1
  8. package/dist/analysis/findings.js +52 -2
  9. package/dist/analysis/findings.js.map +1 -1
  10. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  11. package/dist/analysis/passes/scan-secrets-pass.js +37 -4
  12. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  13. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  14. package/dist/analysis/passes/sink-filter-pass.js +10 -1
  15. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  16. package/dist/analysis/passes/sink-semantics-pass.d.ts +52 -0
  17. package/dist/analysis/passes/sink-semantics-pass.d.ts.map +1 -0
  18. package/dist/analysis/passes/sink-semantics-pass.js +100 -0
  19. package/dist/analysis/passes/sink-semantics-pass.js.map +1 -0
  20. package/dist/analysis/passes/source-semantics-pass.d.ts +66 -0
  21. package/dist/analysis/passes/source-semantics-pass.d.ts.map +1 -0
  22. package/dist/analysis/passes/source-semantics-pass.js +165 -0
  23. package/dist/analysis/passes/source-semantics-pass.js.map +1 -0
  24. package/dist/analysis/passes/taint-propagation-pass.js +60 -9
  25. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  26. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  27. package/dist/analysis/taint-matcher.js +10 -0
  28. package/dist/analysis/taint-matcher.js.map +1 -1
  29. package/dist/analyzer.d.ts.map +1 -1
  30. package/dist/analyzer.js +14 -0
  31. package/dist/analyzer.js.map +1 -1
  32. package/dist/browser/circle-ir.js +295 -17
  33. package/dist/core/circle-ir-core.cjs +142 -11
  34. package/dist/core/circle-ir-core.js +142 -11
  35. package/dist/core/extractors/calls.js +104 -18
  36. package/dist/core/extractors/calls.js.map +1 -1
  37. package/dist/types/config.d.ts +47 -0
  38. package/dist/types/config.d.ts.map +1 -1
  39. package/dist/types/index.d.ts +43 -0
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/package.json +1 -1
@@ -6383,6 +6383,12 @@ function resolveReceiverType(receiver, context) {
6383
6383
  return { simpleName: null, fqn: null };
6384
6384
  }
6385
6385
  if (receiver === "super") return { simpleName: null, fqn: null };
6386
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
6387
+ if (ctorMatch) {
6388
+ const ctorClass = ctorMatch[1];
6389
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
6390
+ return resolveFqn(simple, context);
6391
+ }
6386
6392
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
6387
6393
  if (declaredType) {
6388
6394
  return resolveFqn(stripGenerics(declaredType), context);
@@ -6393,19 +6399,43 @@ function resolveReceiverType(receiver, context) {
6393
6399
  return resolveFqn(simple, context);
6394
6400
  }
6395
6401
  }
6396
- const chained = receiver.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\([^()]*\)$/);
6397
- if (chained) {
6398
- const varName = chained[1];
6399
- const methodName = chained[2];
6400
- const varType = context.localVarTypes.get(varName) ?? context.paramTypes.get(varName) ?? context.fieldTypes.get(varName);
6401
- if (varType) {
6402
- const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[stripGenerics(varType)]?.[methodName];
6402
+ const chain = splitChainedReceiver(receiver);
6403
+ if (chain) {
6404
+ const prefixType = resolveReceiverType(chain.prefix, context);
6405
+ if (prefixType.simpleName) {
6406
+ const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[prefixType.simpleName]?.[chain.methodName];
6403
6407
  if (returnType) return resolveFqn(returnType, context);
6404
6408
  }
6405
6409
  }
6406
6410
  return { simpleName: null, fqn: null };
6407
6411
  }
6412
+ function splitChainedReceiver(receiver) {
6413
+ if (!receiver.endsWith(")")) return null;
6414
+ let depth = 0;
6415
+ let openIdx = -1;
6416
+ for (let i2 = receiver.length - 1; i2 >= 0; i2--) {
6417
+ const c = receiver[i2];
6418
+ if (c === ")") depth++;
6419
+ else if (c === "(") {
6420
+ depth--;
6421
+ if (depth === 0) {
6422
+ openIdx = i2;
6423
+ break;
6424
+ }
6425
+ }
6426
+ }
6427
+ if (openIdx <= 0) return null;
6428
+ const beforeParen = receiver.substring(0, openIdx);
6429
+ const dotIdx = beforeParen.lastIndexOf(".");
6430
+ if (dotIdx <= 0) return null;
6431
+ const methodName = beforeParen.substring(dotIdx + 1).trim();
6432
+ if (!/^[A-Za-z_$][\w$]*$/.test(methodName)) return null;
6433
+ const prefix = beforeParen.substring(0, dotIdx).trim();
6434
+ if (!prefix) return null;
6435
+ return { prefix, methodName };
6436
+ }
6408
6437
  var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
6438
+ // Servlet API (Sprint 91 / #117)
6409
6439
  HttpServletRequest: {
6410
6440
  getSession: "HttpSession",
6411
6441
  getServletContext: "ServletContext",
@@ -6416,6 +6446,32 @@ var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
6416
6446
  },
6417
6447
  ServletContext: {
6418
6448
  getRequestDispatcher: "RequestDispatcher"
6449
+ },
6450
+ // JAXP DOM / SAX / XPath / Transformer / StAX factories (Sprint 92 / #189)
6451
+ DocumentBuilderFactory: {
6452
+ newInstance: "DocumentBuilderFactory",
6453
+ newDocumentBuilder: "DocumentBuilder"
6454
+ },
6455
+ SAXParserFactory: {
6456
+ newInstance: "SAXParserFactory",
6457
+ newSAXParser: "SAXParser"
6458
+ },
6459
+ SAXParser: {
6460
+ getXMLReader: "XMLReader"
6461
+ },
6462
+ XPathFactory: {
6463
+ newInstance: "XPathFactory",
6464
+ newXPath: "XPath"
6465
+ },
6466
+ TransformerFactory: {
6467
+ newInstance: "TransformerFactory",
6468
+ newTransformer: "Transformer"
6469
+ },
6470
+ XMLInputFactory: {
6471
+ newInstance: "XMLInputFactory",
6472
+ newFactory: "XMLInputFactory",
6473
+ createXMLStreamReader: "XMLStreamReader",
6474
+ createXMLEventReader: "XMLEventReader"
6419
6475
  }
6420
6476
  };
6421
6477
  function resolveFqn(simpleName, context) {
@@ -10059,12 +10115,25 @@ function loadSinkConfigs(configs) {
10059
10115
  }
10060
10116
  return { sinks, sanitizers };
10061
10117
  }
10062
- function createTaintConfig(sourceContents, sinkContents) {
10118
+ function loadSinkSemanticsConfigs(configs) {
10119
+ const entries = [];
10120
+ for (const config of configs) {
10121
+ if (config.sinks) {
10122
+ entries.push(...config.sinks);
10123
+ }
10124
+ }
10125
+ return entries;
10126
+ }
10127
+ function createTaintConfig(sourceContents, sinkContents, sinkSemanticsContents = []) {
10063
10128
  const sourceConfigs = sourceContents.map((c) => parseConfig(c));
10064
10129
  const sinkConfigs = sinkContents.map((c) => parseConfig(c));
10130
+ const sinkSemanticsConfigs = sinkSemanticsContents.map(
10131
+ (c) => parseConfig(c)
10132
+ );
10065
10133
  const sources = loadSourceConfigs(sourceConfigs);
10066
10134
  const { sinks, sanitizers } = loadSinkConfigs(sinkConfigs);
10067
- return { sources, sinks, sanitizers };
10135
+ const sinkSemantics = loadSinkSemanticsConfigs(sinkSemanticsConfigs);
10136
+ return { sources, sinks, sanitizers, sinkSemantics };
10068
10137
  }
10069
10138
  var DEFAULT_SOURCES = [
10070
10139
  // HTTP Sources (Servlet API)
@@ -10783,6 +10852,14 @@ var DEFAULT_SINKS = [
10783
10852
  // reflection remains a downstream concern of body-writing sinks.
10784
10853
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10785
10854
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10855
+ // Cookie constructor + addCookie — HTTP response splitting via cookie
10856
+ // name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
10857
+ // where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
10858
+ // CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
10859
+ // and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
10860
+ // and inline-ctor shapes both surface.
10861
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
10862
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
10786
10863
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10787
10864
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
10788
10865
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12222,11 +12299,62 @@ var DEFAULT_SANITIZERS = [
12222
12299
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12223
12300
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12224
12301
  ];
12302
+ var DEFAULT_SINK_SEMANTICS = [
12303
+ {
12304
+ signature: "Jedis#executeCommand",
12305
+ real_class: "db_protocol",
12306
+ overrides: ["command_injection", "code_injection"],
12307
+ note: "Redis wire-protocol serialization, not OS exec"
12308
+ },
12309
+ {
12310
+ signature: "Connection#executeCommand",
12311
+ real_class: "db_protocol",
12312
+ overrides: ["command_injection", "code_injection"],
12313
+ note: "Jedis abstract Connection base"
12314
+ },
12315
+ {
12316
+ signature: "JedisCluster#executeCommand",
12317
+ real_class: "db_protocol",
12318
+ overrides: ["command_injection", "code_injection"],
12319
+ note: "Jedis cluster client"
12320
+ },
12321
+ {
12322
+ signature: "Func1#exec",
12323
+ real_class: "functional_dispatch",
12324
+ overrides: ["command_injection", "code_injection"],
12325
+ note: "RxJava functional dispatch, not OS exec"
12326
+ },
12327
+ {
12328
+ signature: "Action0#call",
12329
+ real_class: "functional_dispatch",
12330
+ overrides: ["command_injection"],
12331
+ note: "RxJava Action0 dispatch"
12332
+ },
12333
+ {
12334
+ signature: "Action1#call",
12335
+ real_class: "functional_dispatch",
12336
+ overrides: ["command_injection"],
12337
+ note: "RxJava Action1 dispatch"
12338
+ },
12339
+ {
12340
+ signature: "Unsafe#defineAnonymousClass",
12341
+ real_class: "jdk_internal",
12342
+ overrides: ["code_injection"],
12343
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
12344
+ },
12345
+ {
12346
+ signature: "MethodHandle#invokeExact",
12347
+ real_class: "jdk_internal",
12348
+ overrides: ["code_injection"],
12349
+ note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
12350
+ }
12351
+ ];
12225
12352
  function getDefaultConfig() {
12226
12353
  return {
12227
12354
  sources: DEFAULT_SOURCES,
12228
12355
  sinks: DEFAULT_SINKS,
12229
- sanitizers: DEFAULT_SANITIZERS
12356
+ sanitizers: DEFAULT_SANITIZERS,
12357
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12230
12358
  };
12231
12359
  }
12232
12360
 
@@ -12983,6 +13111,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12983
13111
  const confidence = calculateSinkConfidence(call, pattern);
12984
13112
  const existing = sinkMap.get(key);
12985
13113
  if (!existing || confidence > existing.confidence) {
13114
+ const receiverType = call.receiver_type;
13115
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
12986
13116
  sinkMap.set(key, {
12987
13117
  type: pattern.type,
12988
13118
  cwe: pattern.cwe,
@@ -12990,7 +13120,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12990
13120
  line: call.location.line,
12991
13121
  confidence,
12992
13122
  method: call.method_name,
12993
- argPositions: pattern.arg_positions
13123
+ argPositions: pattern.arg_positions,
13124
+ class: simpleClass
12994
13125
  });
12995
13126
  }
12996
13127
  }
@@ -6317,6 +6317,12 @@ function resolveReceiverType(receiver, context) {
6317
6317
  return { simpleName: null, fqn: null };
6318
6318
  }
6319
6319
  if (receiver === "super") return { simpleName: null, fqn: null };
6320
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
6321
+ if (ctorMatch) {
6322
+ const ctorClass = ctorMatch[1];
6323
+ const simple = ctorClass.includes(".") ? ctorClass.substring(ctorClass.lastIndexOf(".") + 1) : ctorClass;
6324
+ return resolveFqn(simple, context);
6325
+ }
6320
6326
  const declaredType = context.localVarTypes.get(receiver) ?? context.paramTypes.get(receiver) ?? context.fieldTypes.get(receiver);
6321
6327
  if (declaredType) {
6322
6328
  return resolveFqn(stripGenerics(declaredType), context);
@@ -6327,19 +6333,43 @@ function resolveReceiverType(receiver, context) {
6327
6333
  return resolveFqn(simple, context);
6328
6334
  }
6329
6335
  }
6330
- const chained = receiver.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\([^()]*\)$/);
6331
- if (chained) {
6332
- const varName = chained[1];
6333
- const methodName = chained[2];
6334
- const varType = context.localVarTypes.get(varName) ?? context.paramTypes.get(varName) ?? context.fieldTypes.get(varName);
6335
- if (varType) {
6336
- const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[stripGenerics(varType)]?.[methodName];
6336
+ const chain = splitChainedReceiver(receiver);
6337
+ if (chain) {
6338
+ const prefixType = resolveReceiverType(chain.prefix, context);
6339
+ if (prefixType.simpleName) {
6340
+ const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[prefixType.simpleName]?.[chain.methodName];
6337
6341
  if (returnType) return resolveFqn(returnType, context);
6338
6342
  }
6339
6343
  }
6340
6344
  return { simpleName: null, fqn: null };
6341
6345
  }
6346
+ function splitChainedReceiver(receiver) {
6347
+ if (!receiver.endsWith(")")) return null;
6348
+ let depth = 0;
6349
+ let openIdx = -1;
6350
+ for (let i2 = receiver.length - 1; i2 >= 0; i2--) {
6351
+ const c = receiver[i2];
6352
+ if (c === ")") depth++;
6353
+ else if (c === "(") {
6354
+ depth--;
6355
+ if (depth === 0) {
6356
+ openIdx = i2;
6357
+ break;
6358
+ }
6359
+ }
6360
+ }
6361
+ if (openIdx <= 0) return null;
6362
+ const beforeParen = receiver.substring(0, openIdx);
6363
+ const dotIdx = beforeParen.lastIndexOf(".");
6364
+ if (dotIdx <= 0) return null;
6365
+ const methodName = beforeParen.substring(dotIdx + 1).trim();
6366
+ if (!/^[A-Za-z_$][\w$]*$/.test(methodName)) return null;
6367
+ const prefix = beforeParen.substring(0, dotIdx).trim();
6368
+ if (!prefix) return null;
6369
+ return { prefix, methodName };
6370
+ }
6342
6371
  var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
6372
+ // Servlet API (Sprint 91 / #117)
6343
6373
  HttpServletRequest: {
6344
6374
  getSession: "HttpSession",
6345
6375
  getServletContext: "ServletContext",
@@ -6350,6 +6380,32 @@ var JAVA_CHAINED_FACTORY_RETURN_TYPES = {
6350
6380
  },
6351
6381
  ServletContext: {
6352
6382
  getRequestDispatcher: "RequestDispatcher"
6383
+ },
6384
+ // JAXP DOM / SAX / XPath / Transformer / StAX factories (Sprint 92 / #189)
6385
+ DocumentBuilderFactory: {
6386
+ newInstance: "DocumentBuilderFactory",
6387
+ newDocumentBuilder: "DocumentBuilder"
6388
+ },
6389
+ SAXParserFactory: {
6390
+ newInstance: "SAXParserFactory",
6391
+ newSAXParser: "SAXParser"
6392
+ },
6393
+ SAXParser: {
6394
+ getXMLReader: "XMLReader"
6395
+ },
6396
+ XPathFactory: {
6397
+ newInstance: "XPathFactory",
6398
+ newXPath: "XPath"
6399
+ },
6400
+ TransformerFactory: {
6401
+ newInstance: "TransformerFactory",
6402
+ newTransformer: "Transformer"
6403
+ },
6404
+ XMLInputFactory: {
6405
+ newInstance: "XMLInputFactory",
6406
+ newFactory: "XMLInputFactory",
6407
+ createXMLStreamReader: "XMLStreamReader",
6408
+ createXMLEventReader: "XMLEventReader"
6353
6409
  }
6354
6410
  };
6355
6411
  function resolveFqn(simpleName, context) {
@@ -9993,12 +10049,25 @@ function loadSinkConfigs(configs) {
9993
10049
  }
9994
10050
  return { sinks, sanitizers };
9995
10051
  }
9996
- function createTaintConfig(sourceContents, sinkContents) {
10052
+ function loadSinkSemanticsConfigs(configs) {
10053
+ const entries = [];
10054
+ for (const config of configs) {
10055
+ if (config.sinks) {
10056
+ entries.push(...config.sinks);
10057
+ }
10058
+ }
10059
+ return entries;
10060
+ }
10061
+ function createTaintConfig(sourceContents, sinkContents, sinkSemanticsContents = []) {
9997
10062
  const sourceConfigs = sourceContents.map((c) => parseConfig(c));
9998
10063
  const sinkConfigs = sinkContents.map((c) => parseConfig(c));
10064
+ const sinkSemanticsConfigs = sinkSemanticsContents.map(
10065
+ (c) => parseConfig(c)
10066
+ );
9999
10067
  const sources = loadSourceConfigs(sourceConfigs);
10000
10068
  const { sinks, sanitizers } = loadSinkConfigs(sinkConfigs);
10001
- return { sources, sinks, sanitizers };
10069
+ const sinkSemantics = loadSinkSemanticsConfigs(sinkSemanticsConfigs);
10070
+ return { sources, sinks, sanitizers, sinkSemantics };
10002
10071
  }
10003
10072
  var DEFAULT_SOURCES = [
10004
10073
  // HTTP Sources (Servlet API)
@@ -10717,6 +10786,14 @@ var DEFAULT_SINKS = [
10717
10786
  // reflection remains a downstream concern of body-writing sinks.
10718
10787
  { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10719
10788
  { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10789
+ // Cookie constructor + addCookie — HTTP response splitting via cookie
10790
+ // name/value (CWE-113). Reflects the #189 Sprint 92 V02SetCookie fixture
10791
+ // where `res.addCookie(new Cookie(name, req.getParameter("v")))` allows
10792
+ // CRLF injection into the Set-Cookie header. Both the ctor arg[1] (value)
10793
+ // and the addCookie arg[0] (Cookie handle) are flagged so intermediate-var
10794
+ // and inline-ctor shapes both surface.
10795
+ { method: "Cookie", class: "constructor", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0, 1] },
10796
+ { method: "addCookie", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0] },
10720
10797
  // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10721
10798
  // (CWE-601) further down — see entry near line 1195. CRLF via Location
10722
10799
  // header is a secondary concern; keeping the canonical SSRF entry avoids
@@ -12156,11 +12233,62 @@ var DEFAULT_SANITIZERS = [
12156
12233
  { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12157
12234
  { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12158
12235
  ];
12236
+ var DEFAULT_SINK_SEMANTICS = [
12237
+ {
12238
+ signature: "Jedis#executeCommand",
12239
+ real_class: "db_protocol",
12240
+ overrides: ["command_injection", "code_injection"],
12241
+ note: "Redis wire-protocol serialization, not OS exec"
12242
+ },
12243
+ {
12244
+ signature: "Connection#executeCommand",
12245
+ real_class: "db_protocol",
12246
+ overrides: ["command_injection", "code_injection"],
12247
+ note: "Jedis abstract Connection base"
12248
+ },
12249
+ {
12250
+ signature: "JedisCluster#executeCommand",
12251
+ real_class: "db_protocol",
12252
+ overrides: ["command_injection", "code_injection"],
12253
+ note: "Jedis cluster client"
12254
+ },
12255
+ {
12256
+ signature: "Func1#exec",
12257
+ real_class: "functional_dispatch",
12258
+ overrides: ["command_injection", "code_injection"],
12259
+ note: "RxJava functional dispatch, not OS exec"
12260
+ },
12261
+ {
12262
+ signature: "Action0#call",
12263
+ real_class: "functional_dispatch",
12264
+ overrides: ["command_injection"],
12265
+ note: "RxJava Action0 dispatch"
12266
+ },
12267
+ {
12268
+ signature: "Action1#call",
12269
+ real_class: "functional_dispatch",
12270
+ overrides: ["command_injection"],
12271
+ note: "RxJava Action1 dispatch"
12272
+ },
12273
+ {
12274
+ signature: "Unsafe#defineAnonymousClass",
12275
+ real_class: "jdk_internal",
12276
+ overrides: ["code_injection"],
12277
+ note: "sun.misc.Unsafe JDK-internal reflective bridge"
12278
+ },
12279
+ {
12280
+ signature: "MethodHandle#invokeExact",
12281
+ real_class: "jdk_internal",
12282
+ overrides: ["code_injection"],
12283
+ note: "java.lang.invoke.MethodHandle \u2014 JDK-internal"
12284
+ }
12285
+ ];
12159
12286
  function getDefaultConfig() {
12160
12287
  return {
12161
12288
  sources: DEFAULT_SOURCES,
12162
12289
  sinks: DEFAULT_SINKS,
12163
- sanitizers: DEFAULT_SANITIZERS
12290
+ sanitizers: DEFAULT_SANITIZERS,
12291
+ sinkSemantics: DEFAULT_SINK_SEMANTICS
12164
12292
  };
12165
12293
  }
12166
12294
 
@@ -12917,6 +13045,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12917
13045
  const confidence = calculateSinkConfidence(call, pattern);
12918
13046
  const existing = sinkMap.get(key);
12919
13047
  if (!existing || confidence > existing.confidence) {
13048
+ const receiverType = call.receiver_type;
13049
+ const simpleClass = receiverType ? receiverType.split(".").pop() || void 0 : void 0;
12920
13050
  sinkMap.set(key, {
12921
13051
  type: pattern.type,
12922
13052
  cwe: pattern.cwe,
@@ -12924,7 +13054,8 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12924
13054
  line: call.location.line,
12925
13055
  confidence,
12926
13056
  method: call.method_name,
12927
- argPositions: pattern.arg_positions
13057
+ argPositions: pattern.arg_positions,
13058
+ class: simpleClass
12928
13059
  });
12929
13060
  }
12930
13061
  }
@@ -828,6 +828,20 @@ function resolveReceiverType(receiver, context) {
828
828
  // `super` — defer; cannot determine parent class without hierarchy
829
829
  if (receiver === 'super')
830
830
  return { simpleName: null, fqn: null };
831
+ // Constructor receiver: `new X(...)` or `new pkg.X(...)` — resolves to the
832
+ // constructed class. Enables sink matching on inline receivers built via
833
+ // `new Yaml().load(taint)` (#189 Sprint 92 SnakeYAML) or
834
+ // `new ObjectMapper().readValue(taint)` (#189 Sprint 92 Jackson). Nested
835
+ // constructor arguments are ignored — only the outer class name matters
836
+ // for method-receiver resolution.
837
+ const ctorMatch = receiver.match(/^new\s+([A-Za-z_$][\w$.]*)\s*[<(]/);
838
+ if (ctorMatch) {
839
+ const ctorClass = ctorMatch[1];
840
+ const simple = ctorClass.includes('.')
841
+ ? ctorClass.substring(ctorClass.lastIndexOf('.') + 1)
842
+ : ctorClass;
843
+ return resolveFqn(simple, context);
844
+ }
831
845
  // Local variable, parameter, or field declared in this file
832
846
  const declaredType = context.localVarTypes.get(receiver) ??
833
847
  context.paramTypes.get(receiver) ??
@@ -845,22 +859,23 @@ function resolveReceiverType(receiver, context) {
845
859
  return resolveFqn(simple, context);
846
860
  }
847
861
  }
848
- // Chained receiver: `<var>.<method>(...)` — resolve the return type via the
849
- // servlet factory map so sink patterns keyed on the returned class (e.g.
850
- // `HttpSession.setAttribute` for trust_boundary) match receivers built from
851
- // `req.getSession()` / `req.getSession(false)`. cognium-dev #117 Sprint 91:
852
- // 0% recall on OWASP Java trust-boundary category because chained factory
853
- // calls produced receiver_type=null, dropping trust_boundary sink matches
854
- // while xss's classless setAttribute pattern still fired at the same line.
855
- const chained = receiver.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\([^()]*\)$/);
856
- if (chained) {
857
- const varName = chained[1];
858
- const methodName = chained[2];
859
- const varType = context.localVarTypes.get(varName) ??
860
- context.paramTypes.get(varName) ??
861
- context.fieldTypes.get(varName);
862
- if (varType) {
863
- const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[stripGenerics(varType)]?.[methodName];
862
+ // Chained receiver: `<prefix>.<method>(...)` — resolve the return type via
863
+ // the servlet/JAXP factory map so sink patterns keyed on the returned class
864
+ // (e.g. `HttpSession.setAttribute` for trust_boundary, `DocumentBuilder.parse`
865
+ // for xxe) match receivers built from chained factory calls like
866
+ // `req.getSession().setAttribute(...)` (#117 Sprint 91) or
867
+ // `DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(...)`
868
+ // (#189 Sprint 92 JAXP factory chains).
869
+ //
870
+ // The prefix can itself be a chained call: recursion terminates on a bare
871
+ // identifier (local var / param / field) or a static class reference. Each
872
+ // step's return type is looked up in JAVA_CHAINED_FACTORY_RETURN_TYPES so
873
+ // multi-level factory chains like `X.newInstance().newY()` resolve to Y.
874
+ const chain = splitChainedReceiver(receiver);
875
+ if (chain) {
876
+ const prefixType = resolveReceiverType(chain.prefix, context);
877
+ if (prefixType.simpleName) {
878
+ const returnType = JAVA_CHAINED_FACTORY_RETURN_TYPES[prefixType.simpleName]?.[chain.methodName];
864
879
  if (returnType)
865
880
  return resolveFqn(returnType, context);
866
881
  }
@@ -868,7 +883,45 @@ function resolveReceiverType(receiver, context) {
868
883
  return { simpleName: null, fqn: null };
869
884
  }
870
885
  /**
871
- * Chained factory-method return types for Java servlet/JSP APIs.
886
+ * Split a chained receiver expression into `<prefix>.<methodName>(...)`.
887
+ * Walks the string from the right to find the matching open-paren of the
888
+ * outer call, then finds the last `.` before it to isolate the method name
889
+ * and prefix expression. Returns `null` when the receiver is not a method
890
+ * invocation shape (bare identifier, dotted class name, `this.field`, etc.).
891
+ */
892
+ function splitChainedReceiver(receiver) {
893
+ if (!receiver.endsWith(')'))
894
+ return null;
895
+ let depth = 0;
896
+ let openIdx = -1;
897
+ for (let i = receiver.length - 1; i >= 0; i--) {
898
+ const c = receiver[i];
899
+ if (c === ')')
900
+ depth++;
901
+ else if (c === '(') {
902
+ depth--;
903
+ if (depth === 0) {
904
+ openIdx = i;
905
+ break;
906
+ }
907
+ }
908
+ }
909
+ if (openIdx <= 0)
910
+ return null;
911
+ const beforeParen = receiver.substring(0, openIdx);
912
+ const dotIdx = beforeParen.lastIndexOf('.');
913
+ if (dotIdx <= 0)
914
+ return null;
915
+ const methodName = beforeParen.substring(dotIdx + 1).trim();
916
+ if (!/^[A-Za-z_$][\w$]*$/.test(methodName))
917
+ return null;
918
+ const prefix = beforeParen.substring(0, dotIdx).trim();
919
+ if (!prefix)
920
+ return null;
921
+ return { prefix, methodName };
922
+ }
923
+ /**
924
+ * Chained factory-method return types for Java servlet / JAXP / JSP APIs.
872
925
  *
873
926
  * Keyed as `receiverType -> methodName -> returnType`. Used by
874
927
  * `resolveReceiverType` to pin the type of a receiver written as
@@ -876,11 +929,18 @@ function resolveReceiverType(receiver, context) {
876
929
  * recognise `getSession()` as producing `HttpSession` and thus fire the
877
930
  * class-scoped `HttpSession.setAttribute` trust_boundary sink pattern.
878
931
  *
879
- * Deliberately narrow: only the servlet-container APIs where the return
932
+ * The `resolveReceiverType` walker is recursive (via `splitChainedReceiver`),
933
+ * so multi-level chains like
934
+ * `DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(...)`
935
+ * resolve to `DocumentBuilder` and fire the class-scoped
936
+ * `DocumentBuilder.parse` xxe sink pattern (#189 Sprint 92).
937
+ *
938
+ * Deliberately narrow: only well-known JDK factory APIs where the return
880
939
  * type is defined by the interface contract are listed. Application-level
881
940
  * factories (`someService.getFoo()`) are still unresolved.
882
941
  */
883
942
  const JAVA_CHAINED_FACTORY_RETURN_TYPES = {
943
+ // Servlet API (Sprint 91 / #117)
884
944
  HttpServletRequest: {
885
945
  getSession: 'HttpSession',
886
946
  getServletContext: 'ServletContext',
@@ -892,6 +952,32 @@ const JAVA_CHAINED_FACTORY_RETURN_TYPES = {
892
952
  ServletContext: {
893
953
  getRequestDispatcher: 'RequestDispatcher',
894
954
  },
955
+ // JAXP DOM / SAX / XPath / Transformer / StAX factories (Sprint 92 / #189)
956
+ DocumentBuilderFactory: {
957
+ newInstance: 'DocumentBuilderFactory',
958
+ newDocumentBuilder: 'DocumentBuilder',
959
+ },
960
+ SAXParserFactory: {
961
+ newInstance: 'SAXParserFactory',
962
+ newSAXParser: 'SAXParser',
963
+ },
964
+ SAXParser: {
965
+ getXMLReader: 'XMLReader',
966
+ },
967
+ XPathFactory: {
968
+ newInstance: 'XPathFactory',
969
+ newXPath: 'XPath',
970
+ },
971
+ TransformerFactory: {
972
+ newInstance: 'TransformerFactory',
973
+ newTransformer: 'Transformer',
974
+ },
975
+ XMLInputFactory: {
976
+ newInstance: 'XMLInputFactory',
977
+ newFactory: 'XMLInputFactory',
978
+ createXMLStreamReader: 'XMLStreamReader',
979
+ createXMLEventReader: 'XMLEventReader',
980
+ },
895
981
  };
896
982
  /**
897
983
  * Look up the FQN of a simple type name in the file's imports map.