cognium-dev 3.135.0 → 3.136.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 +256 -1
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -22643,6 +22643,12 @@ class LanguageSourcesPass {
22643
22643
  for (const finding of findPythonMongoengineWhereNosqlInjectionFindings(code, graph.ir.meta.file)) {
22644
22644
  ctx.addFinding(finding);
22645
22645
  }
22646
+ for (const finding of findPythonTaintedFormatStringFindings(code, graph.ir.meta.file)) {
22647
+ ctx.addFinding(finding);
22648
+ }
22649
+ for (const finding of findPythonHeaderCrlfInjectionFindings(code, graph.ir.meta.file)) {
22650
+ ctx.addFinding(finding);
22651
+ }
22646
22652
  }
22647
22653
  if (language === "rust") {
22648
22654
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -22694,6 +22700,9 @@ class LanguageSourcesPass {
22694
22700
  for (const finding of findJsIndirectEvalCodeInjectionFindings(code, graph.ir.meta.file)) {
22695
22701
  ctx.addFinding(finding);
22696
22702
  }
22703
+ for (const finding of findJsUtilFormatFormatStringFindings(code, graph.ir.meta.file)) {
22704
+ ctx.addFinding(finding);
22705
+ }
22697
22706
  }
22698
22707
  if (language === "java") {
22699
22708
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -27036,6 +27045,252 @@ function findJavaUrlOpenStreamSsrfFindings(code, file) {
27036
27045
  }
27037
27046
  return findings;
27038
27047
  }
27048
+ function findPythonTaintedFormatStringFindings(code, file) {
27049
+ const findings = [];
27050
+ if (typeof code !== "string" || code.length === 0)
27051
+ return findings;
27052
+ if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(code)) {
27053
+ return findings;
27054
+ }
27055
+ const lines = code.split(`
27056
+ `);
27057
+ const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
27058
+ const taintedVars = new Set;
27059
+ for (let pass = 0;pass < 3; pass++) {
27060
+ const before = taintedVars.size;
27061
+ for (let i2 = 0;i2 < lines.length; i2++) {
27062
+ const t = lines[i2].trim();
27063
+ if (t.startsWith("#"))
27064
+ continue;
27065
+ const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
27066
+ if (!a)
27067
+ continue;
27068
+ const lhs = a[1];
27069
+ const rhs = a[2];
27070
+ if (taintedVars.has(lhs))
27071
+ continue;
27072
+ if (reqExtractRe.test(rhs)) {
27073
+ taintedVars.add(lhs);
27074
+ continue;
27075
+ }
27076
+ for (const v of taintedVars) {
27077
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27078
+ taintedVars.add(lhs);
27079
+ break;
27080
+ }
27081
+ }
27082
+ }
27083
+ if (taintedVars.size === before)
27084
+ break;
27085
+ }
27086
+ if (taintedVars.size === 0)
27087
+ return findings;
27088
+ const percentRe = /\b(\w+)\s*%\s*[\(\[\{"'\w]/;
27089
+ const dotFormatRe = /\b(\w+)\s*\.\s*format\s*\(/;
27090
+ const seen = new Set;
27091
+ for (let i2 = 0;i2 < lines.length; i2++) {
27092
+ const line = lines[i2];
27093
+ const t = line.trim();
27094
+ if (t.startsWith("#"))
27095
+ continue;
27096
+ const pm = line.match(percentRe);
27097
+ if (pm && taintedVars.has(pm[1])) {
27098
+ const key = `${i2 + 1}:percent`;
27099
+ if (!seen.has(key)) {
27100
+ seen.add(key);
27101
+ findings.push({
27102
+ id: `format_string-${file}-${i2 + 1}-py-percent`,
27103
+ pass: "language-sources",
27104
+ category: "security",
27105
+ rule_id: "format_string",
27106
+ cwe: "CWE-134",
27107
+ severity: "high",
27108
+ level: "error",
27109
+ message: "Format-string injection: Python `<tainted> % args` uses an " + "HTTP-request-controlled format string. Malformed specifiers can " + "crash the handler (TypeError) and `%(name)s` access can reveal " + "arbitrary mapping keys. Use a literal format string and pass " + "untrusted values as arguments only.",
27110
+ file,
27111
+ line: i2 + 1,
27112
+ snippet: line.trim()
27113
+ });
27114
+ }
27115
+ }
27116
+ const dm = line.match(dotFormatRe);
27117
+ if (dm && taintedVars.has(dm[1])) {
27118
+ const key = `${i2 + 1}:dotformat`;
27119
+ if (!seen.has(key)) {
27120
+ seen.add(key);
27121
+ findings.push({
27122
+ id: `format_string-${file}-${i2 + 1}-py-strformat`,
27123
+ pass: "language-sources",
27124
+ category: "security",
27125
+ rule_id: "format_string",
27126
+ cwe: "CWE-134",
27127
+ severity: "high",
27128
+ level: "error",
27129
+ message: "Format-string injection: Python `<tainted>.format(...)` lets " + "the attacker control the format template. Field-access shapes " + "such as `{0.__class__.__init__.__globals__}` can leak module " + "globals (e.g. secret keys). Use a literal template and pass " + "untrusted values as positional/keyword arguments only.",
27130
+ file,
27131
+ line: i2 + 1,
27132
+ snippet: line.trim()
27133
+ });
27134
+ }
27135
+ }
27136
+ }
27137
+ return findings;
27138
+ }
27139
+ function findJsUtilFormatFormatStringFindings(code, file) {
27140
+ const findings = [];
27141
+ if (typeof code !== "string" || code.length === 0)
27142
+ return findings;
27143
+ if (!/\butil\s*\.\s*format\s*\(/.test(code))
27144
+ return findings;
27145
+ if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
27146
+ return findings;
27147
+ }
27148
+ const lines = code.split(`
27149
+ `);
27150
+ const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
27151
+ const taintedVars = new Set;
27152
+ for (let pass = 0;pass < 3; pass++) {
27153
+ const before = taintedVars.size;
27154
+ for (let i2 = 0;i2 < lines.length; i2++) {
27155
+ const t = lines[i2].trim();
27156
+ if (t.startsWith("//"))
27157
+ continue;
27158
+ const a = t.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?$/);
27159
+ if (!a)
27160
+ continue;
27161
+ const lhs = a[1];
27162
+ const rhs = a[2];
27163
+ if (taintedVars.has(lhs))
27164
+ continue;
27165
+ if (reqExtractRe.test(rhs)) {
27166
+ taintedVars.add(lhs);
27167
+ continue;
27168
+ }
27169
+ for (const v of taintedVars) {
27170
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27171
+ taintedVars.add(lhs);
27172
+ break;
27173
+ }
27174
+ }
27175
+ }
27176
+ if (taintedVars.size === before)
27177
+ break;
27178
+ }
27179
+ if (taintedVars.size === 0)
27180
+ return findings;
27181
+ const callRe = /\butil\s*\.\s*format\s*\(\s*([\w.]+)\s*[,)]/;
27182
+ for (let i2 = 0;i2 < lines.length; i2++) {
27183
+ const line = lines[i2];
27184
+ const m = line.match(callRe);
27185
+ if (!m)
27186
+ continue;
27187
+ const arg = m[1];
27188
+ const root = arg.split(".")[0];
27189
+ if (!taintedVars.has(root) && !reqExtractRe.test(arg))
27190
+ continue;
27191
+ findings.push({
27192
+ id: `format_string-${file}-${i2 + 1}-js-util-format`,
27193
+ pass: "language-sources",
27194
+ category: "security",
27195
+ rule_id: "format_string",
27196
+ cwe: "CWE-134",
27197
+ severity: "medium",
27198
+ level: "error",
27199
+ message: "Format-string injection: Node `util.format(<tainted>, ...)` uses " + "an HTTP-request-controlled format string. The attacker can " + "manipulate `%s`/`%j`/`%O` specifiers to alter the rendered " + "output. Pass user input as a subsequent argument with a literal " + "format string.",
27200
+ file,
27201
+ line: i2 + 1,
27202
+ snippet: line.trim()
27203
+ });
27204
+ }
27205
+ return findings;
27206
+ }
27207
+ function findPythonHeaderCrlfInjectionFindings(code, file) {
27208
+ const findings = [];
27209
+ if (typeof code !== "string" || code.length === 0)
27210
+ return findings;
27211
+ if (!/\b\w+\s*\.\s*headers\b/.test(code))
27212
+ return findings;
27213
+ if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(code)) {
27214
+ return findings;
27215
+ }
27216
+ const lines = code.split(`
27217
+ `);
27218
+ const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
27219
+ const taintedVars = new Set;
27220
+ for (let pass = 0;pass < 3; pass++) {
27221
+ const before = taintedVars.size;
27222
+ for (let i2 = 0;i2 < lines.length; i2++) {
27223
+ const t = lines[i2].trim();
27224
+ if (t.startsWith("#"))
27225
+ continue;
27226
+ const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
27227
+ if (!a)
27228
+ continue;
27229
+ const lhs = a[1];
27230
+ const rhs = a[2];
27231
+ if (taintedVars.has(lhs))
27232
+ continue;
27233
+ if (reqExtractRe.test(rhs)) {
27234
+ taintedVars.add(lhs);
27235
+ continue;
27236
+ }
27237
+ for (const v of taintedVars) {
27238
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27239
+ taintedVars.add(lhs);
27240
+ break;
27241
+ }
27242
+ }
27243
+ }
27244
+ if (taintedVars.size === before)
27245
+ break;
27246
+ }
27247
+ if (taintedVars.size === 0)
27248
+ return findings;
27249
+ const subscriptRe = /\b\w+\s*\.\s*headers\s*\[\s*['"][^'"]+['"]\s*\]\s*=\s*(.+)$/;
27250
+ const methodRe = /\b\w+\s*\.\s*headers\s*\.\s*(?:add|set|setdefault|append)\s*\(\s*['"][^'"]+['"]\s*,\s*(.+?)\)/;
27251
+ for (let i2 = 0;i2 < lines.length; i2++) {
27252
+ const line = lines[i2];
27253
+ const t = line.trim();
27254
+ if (t.startsWith("#"))
27255
+ continue;
27256
+ let expr = null;
27257
+ const sm = line.match(subscriptRe);
27258
+ if (sm)
27259
+ expr = sm[1];
27260
+ else {
27261
+ const mm = line.match(methodRe);
27262
+ if (mm)
27263
+ expr = mm[1];
27264
+ }
27265
+ if (!expr)
27266
+ continue;
27267
+ let tainted = reqExtractRe.test(expr);
27268
+ if (!tainted) {
27269
+ for (const v of taintedVars) {
27270
+ if (new RegExp(`\\b${v}\\b`).test(expr)) {
27271
+ tainted = true;
27272
+ break;
27273
+ }
27274
+ }
27275
+ }
27276
+ if (!tainted)
27277
+ continue;
27278
+ findings.push({
27279
+ id: `crlf-${file}-${i2 + 1}-py-headers`,
27280
+ pass: "language-sources",
27281
+ category: "security",
27282
+ rule_id: "crlf",
27283
+ cwe: "CWE-113",
27284
+ severity: "medium",
27285
+ level: "error",
27286
+ message: "CRLF / header injection: Python Flask/Werkzeug " + "`response.headers[...] = <tainted>` lets an attacker inject a " + "`\\r\\n` sequence and forge additional headers or split the " + "response body. Validate the value (reject control characters) or " + "use a fixed allowlist.",
27287
+ file,
27288
+ line: i2 + 1,
27289
+ snippet: line.trim()
27290
+ });
27291
+ }
27292
+ return findings;
27293
+ }
27039
27294
 
27040
27295
  // ../circle-ir/dist/analysis/passes/sink-filter-pass.js
27041
27296
  var JS_XSS_SANITIZERS = [
@@ -40163,7 +40418,7 @@ var colors = {
40163
40418
  };
40164
40419
 
40165
40420
  // src/version.ts
40166
- var version = "3.135.0";
40421
+ var version = "3.136.0";
40167
40422
 
40168
40423
  // src/formatters.ts
40169
40424
  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.135.0",
3
+ "version": "3.136.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.135.0"
69
+ "circle-ir": "^3.136.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",