cognium-dev 3.180.0 → 3.182.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 (2) hide show
  1. package/dist/cli.js +245 -19
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -9440,6 +9440,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
9440
9440
  } else if (child.type === "for_statement") {
9441
9441
  const rangeClause = findChildByTypeGo(child, "range_clause");
9442
9442
  if (rangeClause) {
9443
+ const right = rangeClause.childForFieldName("right");
9444
+ if (right) {
9445
+ extractGoUses(right, uses, scopeStack);
9446
+ }
9443
9447
  const left = rangeClause.childForFieldName("left");
9444
9448
  if (left) {
9445
9449
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
@@ -9447,6 +9451,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
9447
9451
  }
9448
9452
  } else if (child.type === "call_expression") {
9449
9453
  extractGoUses(child, uses, scopeStack);
9454
+ recordGoOpaqueCodecDestDef(child, defs, scopeStack);
9450
9455
  } else if (child.type === "return_statement") {
9451
9456
  for (let i2 = 0;i2 < child.childCount; i2++) {
9452
9457
  const expr = child.child(i2);
@@ -9546,6 +9551,70 @@ function findChildByTypeGo(node, type) {
9546
9551
  }
9547
9552
  return null;
9548
9553
  }
9554
+ var GO_OPAQUE_CODEC_METHODS = new Set([
9555
+ "Unmarshal",
9556
+ "Decode",
9557
+ "NewDecoder",
9558
+ "UnmarshalYAML",
9559
+ "UnmarshalJSON",
9560
+ "UnmarshalText",
9561
+ "UnmarshalBinary"
9562
+ ]);
9563
+ function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
9564
+ const fn = call.childForFieldName("function");
9565
+ if (!fn || fn.type !== "selector_expression")
9566
+ return;
9567
+ const fieldNode = fn.childForFieldName("field");
9568
+ if (!fieldNode)
9569
+ return;
9570
+ const method = getNodeText(fieldNode);
9571
+ if (!GO_OPAQUE_CODEC_METHODS.has(method))
9572
+ return;
9573
+ const argsNode = call.childForFieldName("arguments");
9574
+ if (!argsNode)
9575
+ return;
9576
+ const args2 = [];
9577
+ for (let i2 = 0;i2 < argsNode.childCount; i2++) {
9578
+ const c = argsNode.child(i2);
9579
+ if (!c)
9580
+ continue;
9581
+ if (c.type === "," || c.type === "(" || c.type === ")")
9582
+ continue;
9583
+ args2.push(c);
9584
+ }
9585
+ if (args2.length === 0)
9586
+ return;
9587
+ const destArg = args2.length >= 2 ? args2[1] : args2[0];
9588
+ const destName = extractGoAddressableVarName(destArg);
9589
+ if (!destName || destName === "_")
9590
+ return;
9591
+ const line = call.startPosition.row + 1;
9592
+ const def = {
9593
+ id: defs.length + 1,
9594
+ variable: destName,
9595
+ kind: "local",
9596
+ line
9597
+ };
9598
+ defs.push(def);
9599
+ currentScope(scopeStack).set(destName, def.id);
9600
+ }
9601
+ function extractGoAddressableVarName(node) {
9602
+ if (node.type === "unary_expression") {
9603
+ const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
9604
+ if (operand)
9605
+ return extractGoAddressableVarName(operand);
9606
+ return null;
9607
+ }
9608
+ if (node.type === "identifier") {
9609
+ return getNodeText(node);
9610
+ }
9611
+ if (node.type === "selector_expression") {
9612
+ const field = node.childForFieldName("field");
9613
+ if (field)
9614
+ return getNodeText(field);
9615
+ }
9616
+ return null;
9617
+ }
9549
9618
  // ../circle-ir/dist/core/extractors/runtime-registrations.js
9550
9619
  var HTTP_VERB_METHODS = new Set([
9551
9620
  "get",
@@ -10634,6 +10703,16 @@ var DEFAULT_SOURCES = [
10634
10703
  { method: "get", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10635
10704
  { method: "hget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10636
10705
  { method: "mget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10706
+ { property: "body", object: "event", type: "http_body", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10707
+ { property: "queryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10708
+ { property: "multiValueQueryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10709
+ { property: "pathParameters", object: "event", type: "http_path", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10710
+ { property: "headers", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10711
+ { property: "multiValueHeaders", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10712
+ { property: "body", object: "event", type: "http_body", severity: "high", property_tainted: true, languages: ["python"] },
10713
+ { property: "queryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["python"] },
10714
+ { property: "pathParameters", object: "event", type: "http_path", severity: "high", property_tainted: true, languages: ["python"] },
10715
+ { property: "headers", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["python"] },
10637
10716
  { method: "decode", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["python"] },
10638
10717
  { method: "get_unverified_claims", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["python"] },
10639
10718
  { method: "get_unverified_header", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["python"] },
@@ -16422,15 +16501,23 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
16422
16501
  const callsAtSink = callsByLine.get(sink.line) ?? [];
16423
16502
  for (const call of callsAtSink) {
16424
16503
  for (const arg of call.arguments) {
16425
- if (arg.variable) {
16426
- if (sink.argPositions && sink.argPositions.length > 0) {
16427
- if (!sink.argPositions.includes(arg.position)) {
16428
- continue;
16429
- }
16504
+ if (sink.argPositions && sink.argPositions.length > 0) {
16505
+ if (!sink.argPositions.includes(arg.position)) {
16506
+ continue;
16430
16507
  }
16431
- for (const use of usesAtSink) {
16432
- if (use.variable === arg.variable && use.def_id !== null) {
16508
+ }
16509
+ const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
16510
+ {
16511
+ for (const use of candidateUses) {
16512
+ if (use.def_id !== null) {
16433
16513
  if (allTaintedDefIds.has(use.def_id)) {
16514
+ if (!arg.variable) {
16515
+ if (typeof arg.expression !== "string" || arg.expression.length === 0)
16516
+ continue;
16517
+ const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
16518
+ if (!re.test(arg.expression))
16519
+ continue;
16520
+ }
16434
16521
  const taintInfo = taintByDefId.get(use.def_id);
16435
16522
  if (taintInfo) {
16436
16523
  const isSanitized = checkSanitized(taintInfo.line, sink.line, sink.type, sanitizersByLine, {
@@ -16458,7 +16545,16 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
16458
16545
  }
16459
16546
  }
16460
16547
  }
16461
- return { taintedVars, flows, reachableSinks };
16548
+ const seen = new Set;
16549
+ const deduped = [];
16550
+ for (const f of flows) {
16551
+ const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
16552
+ if (seen.has(key))
16553
+ continue;
16554
+ seen.add(key);
16555
+ deduped.push(f);
16556
+ }
16557
+ return { taintedVars, flows: deduped, reachableSinks };
16462
16558
  }
16463
16559
  function findInitialTaint(sources, callsByLine, defsByLine) {
16464
16560
  const tainted = [];
@@ -24564,7 +24660,13 @@ var JS_TAINTED_PATTERNS = [
24564
24660
  { pattern: /\bdocument\.title\b/, type: "dom_input" },
24565
24661
  { pattern: /\bhistory\.state\b/, type: "dom_input" },
24566
24662
  { pattern: /\blocalStorage\.getItem\b/, type: "dom_input" },
24567
- { pattern: /\bsessionStorage\.getItem\b/, type: "dom_input" }
24663
+ { pattern: /\bsessionStorage\.getItem\b/, type: "dom_input" },
24664
+ { pattern: /\bevent\.body\b/, type: "http_body" },
24665
+ { pattern: /\bevent\.queryStringParameters\b/, type: "http_param" },
24666
+ { pattern: /\bevent\.multiValueQueryStringParameters\b/, type: "http_param" },
24667
+ { pattern: /\bevent\.pathParameters\b/, type: "http_path" },
24668
+ { pattern: /\bevent\.headers\b/, type: "http_header" },
24669
+ { pattern: /\bevent\.multiValueHeaders\b/, type: "http_header" }
24568
24670
  ];
24569
24671
  var PYTHON_TAINTED_PATTERNS2 = [
24570
24672
  { pattern: /\brequest\.args\b/, type: "http_param" },
@@ -29469,7 +29571,7 @@ function findPythonTaintedFormatStringFindings(code, file) {
29469
29571
  if (taintedVars.size === 0)
29470
29572
  return findings;
29471
29573
  const percentRe = /\b(\w+)\s*%\s*[\(\[\{"'\w]/;
29472
- const dotFormatRe = /\b(\w+)\s*\.\s*format\s*\(/;
29574
+ const dotFormatRe = /\b(\w+)\s*\.\s*(?:format|format_map)\s*\(/;
29473
29575
  const seen = new Set;
29474
29576
  for (let i2 = 0;i2 < lines.length; i2++) {
29475
29577
  const line = lines[i2];
@@ -33377,6 +33479,12 @@ class TaintPropagationPass {
33377
33479
  continue;
33378
33480
  pushIfNew(f);
33379
33481
  }
33482
+ if (ctx.language === "go" && typeof ctx.code === "string") {
33483
+ const goGlobalFlows = detectGoPackageGlobalFlows(ctx.code, calls, sources, sinks, constProp.unreachableLines) ?? [];
33484
+ for (const f of goGlobalFlows) {
33485
+ pushIfNew(f);
33486
+ }
33487
+ }
33380
33488
  const sanitizedNames = constProp.sanitizedVars;
33381
33489
  let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
33382
33490
  if (f.path.length === 0)
@@ -33633,6 +33741,113 @@ function isInJavaSanitizedMethod(code, types, sinkLine, sinkType) {
33633
33741
  }
33634
33742
  return false;
33635
33743
  }
33744
+ function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLines) {
33745
+ const flows = [];
33746
+ if (!code || sinks.length === 0)
33747
+ return flows;
33748
+ const lines = code.split(`
33749
+ `);
33750
+ const packageVars = new Set;
33751
+ let inVarBlock = false;
33752
+ for (const line of lines) {
33753
+ const stripped = line.replace(/\s+$/, "");
33754
+ if (!stripped)
33755
+ continue;
33756
+ if (/^[ \t]/.test(line)) {
33757
+ if (inVarBlock) {
33758
+ const m = stripped.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s+/);
33759
+ if (m)
33760
+ packageVars.add(m[1]);
33761
+ if (/^\s*\)/.test(stripped))
33762
+ inVarBlock = false;
33763
+ }
33764
+ continue;
33765
+ }
33766
+ if (/^var\s*\(/.test(stripped)) {
33767
+ inVarBlock = true;
33768
+ continue;
33769
+ }
33770
+ if (inVarBlock && /^\)/.test(stripped)) {
33771
+ inVarBlock = false;
33772
+ continue;
33773
+ }
33774
+ const varOne = stripped.match(/^var\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
33775
+ if (varOne)
33776
+ packageVars.add(varOne[1]);
33777
+ }
33778
+ if (packageVars.size === 0)
33779
+ return flows;
33780
+ const sourceVarNames = new Set;
33781
+ for (const s of sources) {
33782
+ if (typeof s.variable === "string" && s.variable.length > 0) {
33783
+ sourceVarNames.add(s.variable);
33784
+ }
33785
+ }
33786
+ const GO_SOURCE_RE = /\b(r\.(?:URL\.Query|Form|PostForm|Header|Cookie|Body)|mux\.Vars|c\.(?:Query|Param|PostForm|GetHeader|Cookie)|ctx\.(?:Query|Param|PostForm|GetHeader|Cookie))\b/;
33787
+ const writes = [];
33788
+ for (let i2 = 0;i2 < lines.length; i2++) {
33789
+ const line = lines[i2];
33790
+ if (unreachableLines.has(i2 + 1))
33791
+ continue;
33792
+ if (/:=/.test(line))
33793
+ continue;
33794
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
33795
+ if (!m)
33796
+ continue;
33797
+ const [, lhs, rhs] = m;
33798
+ if (!packageVars.has(lhs))
33799
+ continue;
33800
+ let taintedBy = null;
33801
+ if (GO_SOURCE_RE.test(rhs)) {
33802
+ const src = sources.find((s) => s.line === i2 + 1);
33803
+ taintedBy = src ? { line: src.line, type: src.type } : { line: i2 + 1, type: "http_param" };
33804
+ } else {
33805
+ for (const v of sourceVarNames) {
33806
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
33807
+ if (re.test(rhs)) {
33808
+ const src = sources.find((s) => s.variable === v);
33809
+ if (src) {
33810
+ taintedBy = { line: src.line, type: src.type };
33811
+ break;
33812
+ }
33813
+ }
33814
+ }
33815
+ }
33816
+ if (!taintedBy)
33817
+ continue;
33818
+ writes.push({ varName: lhs, line: i2 + 1, sourceLine: taintedBy.line, sourceType: taintedBy.type });
33819
+ }
33820
+ if (writes.length === 0)
33821
+ return flows;
33822
+ for (const sink of sinks) {
33823
+ if (unreachableLines.has(sink.line))
33824
+ continue;
33825
+ const sinkLineText = lines[sink.line - 1] ?? "";
33826
+ if (!sinkLineText)
33827
+ continue;
33828
+ for (const w of writes) {
33829
+ if (w.line >= sink.line)
33830
+ continue;
33831
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${w.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
33832
+ if (!re.test(sinkLineText))
33833
+ continue;
33834
+ flows.push({
33835
+ source_line: w.sourceLine,
33836
+ sink_line: sink.line,
33837
+ source_type: w.sourceType,
33838
+ sink_type: sink.type,
33839
+ path: [
33840
+ { variable: w.varName, line: w.sourceLine, type: "source" },
33841
+ { variable: w.varName, line: w.line, type: "assignment" },
33842
+ { variable: w.varName, line: sink.line, type: "sink" }
33843
+ ],
33844
+ confidence: 0.9,
33845
+ sanitized: false
33846
+ });
33847
+ }
33848
+ }
33849
+ return flows;
33850
+ }
33636
33851
  function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
33637
33852
  const flows = [];
33638
33853
  const callsByLine = new Map;
@@ -39789,30 +40004,41 @@ class PythonReceiverTaintFormatPass {
39789
40004
  if (ctx.language !== "python") {
39790
40005
  return { findingsEmitted: 0 };
39791
40006
  }
39792
- const { calls, meta } = ctx.graph.ir;
39793
- const constProp = ctx.getResult("constant-propagation");
39794
- const tainted = constProp.tainted;
40007
+ const { calls, meta, taint } = ctx.graph.ir;
40008
+ const taintedVarNames = new Set;
40009
+ for (const s of taint.sources) {
40010
+ if (typeof s.variable === "string" && s.variable.length > 0) {
40011
+ taintedVarNames.add(s.variable);
40012
+ }
40013
+ }
40014
+ if (taintedVarNames.size === 0) {
40015
+ return { findingsEmitted: 0 };
40016
+ }
40017
+ const seen = new Set;
39795
40018
  let count = 0;
39796
40019
  for (const call of calls) {
39797
- if (call.method_name !== "format")
40020
+ if (call.method_name !== "format" && call.method_name !== "format_map")
39798
40021
  continue;
39799
40022
  const receiver = call.receiver;
39800
40023
  if (!receiver)
39801
40024
  continue;
39802
40025
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(receiver))
39803
40026
  continue;
39804
- const scopedName = call.in_method ? `${call.in_method}:${receiver}` : receiver;
39805
- if (!tainted.has(receiver) && !tainted.has(scopedName))
40027
+ if (!taintedVarNames.has(receiver))
40028
+ continue;
40029
+ const seenKey = `${call.location.line}:${receiver}`;
40030
+ if (seen.has(seenKey))
39806
40031
  continue;
40032
+ seen.add(seenKey);
39807
40033
  ctx.addFinding({
39808
- id: `format_string-${meta.file}-${call.location.line}-${receiver}`,
40034
+ id: `format_string-${meta.file}-${call.location.line}-${receiver}-${call.method_name}`,
39809
40035
  pass: this.name,
39810
40036
  category: "security",
39811
40037
  rule_id: "format_string",
39812
40038
  cwe: "CWE-134",
39813
40039
  severity: "medium",
39814
40040
  level: "warning",
39815
- message: `Format-string vulnerability: the format template \`${receiver}\` ` + `is tainted (traces back to a taint source). Attacker can leak ` + `object attributes via \`{obj.__class__.__mro__[…]}\` chains or ` + `crash the process via unbounded specifiers. Use a literal ` + `format string and pass user input as an argument instead.`,
40041
+ message: `Format-string vulnerability: the format template \`${receiver}\` ` + `is tainted (traces back to a taint source) and used as the ` + `receiver of \`${call.method_name}(...)\`. Attacker can leak ` + `object attributes via \`{obj.__class__.__mro__[…]}\` chains or ` + `crash the process via unbounded specifiers. Use a literal ` + `format string and pass user input as an argument instead.`,
39816
40042
  file: meta.file,
39817
40043
  line: call.location.line
39818
40044
  });
@@ -45424,7 +45650,7 @@ var colors = {
45424
45650
  };
45425
45651
 
45426
45652
  // src/version.ts
45427
- var version = "3.180.0";
45653
+ var version = "3.182.0";
45428
45654
 
45429
45655
  // src/formatters.ts
45430
45656
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.180.0",
3
+ "version": "3.182.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@cognium/project-profile-detect": "^1.1.0",
69
- "circle-ir": "^3.180.0"
69
+ "circle-ir": "^3.182.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",