cognium-dev 3.81.0 → 3.82.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 +319 -4
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -11478,9 +11478,15 @@ var DEFAULT_SANITIZERS = [
11478
11478
  { method: "sanitize", class: "DOMPurify", removes: ["xss"] },
11479
11479
  { method: "escape", class: "validator", removes: ["xss"] },
11480
11480
  { method: "parse", class: "JSON", removes: ["xss", "code_injection"] },
11481
- { method: "parseInt", removes: ["sql_injection", "nosql_injection", "command_injection", "xss"] },
11482
- { method: "parseFloat", removes: ["sql_injection", "nosql_injection", "command_injection"] },
11483
- { method: "Number", removes: ["sql_injection", "nosql_injection", "command_injection"] },
11481
+ { method: "parseInt", removes: ["sql_injection", "nosql_injection", "command_injection", "xss", "external_taint_escape", "path_traversal", "code_injection"] },
11482
+ { method: "parseFloat", removes: ["sql_injection", "nosql_injection", "command_injection", "external_taint_escape", "path_traversal", "code_injection"] },
11483
+ { method: "Number", removes: ["sql_injection", "nosql_injection", "command_injection", "external_taint_escape", "path_traversal", "code_injection"] },
11484
+ { method: "min", class: "Math", removes: ["external_taint_escape"] },
11485
+ { method: "max", class: "Math", removes: ["external_taint_escape"] },
11486
+ { method: "includes", removes: ["external_taint_escape"] },
11487
+ { method: "has", removes: ["external_taint_escape"] },
11488
+ { method: "contains", removes: ["external_taint_escape"] },
11489
+ { method: "indexOf", removes: ["external_taint_escape"] },
11484
11490
  { method: "basename", class: "path", removes: ["path_traversal"] },
11485
11491
  { method: "normalize", class: "path", removes: ["path_traversal"] },
11486
11492
  { method: "resolve", class: "path", removes: ["path_traversal"] },
@@ -30522,6 +30528,311 @@ class WeakPasswordEncodingPass {
30522
30528
  }
30523
30529
  }
30524
30530
 
30531
+ // ../circle-ir/dist/analysis/passes/info-disclosure-stacktrace-pass.js
30532
+ var RESPONSE_RECEIVER_RE = /^(res|response|w|writer|ctx|c)$/i;
30533
+ var LOGGER_RECEIVER_RE = /^(log|logger|slog|console|pino|winston|sentry)$/i;
30534
+ var RESPONSE_SEND_METHODS = new Set([
30535
+ "send",
30536
+ "json",
30537
+ "write",
30538
+ "writeHead",
30539
+ "end",
30540
+ "sendFile",
30541
+ "println",
30542
+ "print",
30543
+ "getWriter",
30544
+ "Fprintln",
30545
+ "Fprintf",
30546
+ "Fprint"
30547
+ ]);
30548
+ function isExceptionExpression(expr) {
30549
+ if (!expr)
30550
+ return false;
30551
+ const e = expr.trim();
30552
+ return /\b(err|error|exc|exception|e|t|throwable)\.(stack|message|toString\(|getMessage\(|getStackTrace\(|getLocalizedMessage\(|getCause\()/i.test(e) || /\btraceback\.(format_exc|format_exception|print_exc)\b/i.test(e) || /\bdebug\.Stack\(\)/.test(e) || /\bstr\(\s*(err|error|exc|exception|e)\s*\)/i.test(e) || /\bString\(\s*(err|error|exc|exception|e)\s*\)/i.test(e);
30553
+ }
30554
+ function argIsException(arg) {
30555
+ if (!arg)
30556
+ return false;
30557
+ if (arg.variable && /^(err|error|exc|exception|e|t|throwable)$/i.test(arg.variable)) {
30558
+ return true;
30559
+ }
30560
+ return isExceptionExpression(arg.expression);
30561
+ }
30562
+ function detectJavaPrintStackTrace(call) {
30563
+ if (call.method_name !== "printStackTrace")
30564
+ return null;
30565
+ const rec = call.receiver ?? "";
30566
+ if (!/^(e|ex|exc|exception|err|error|t|throwable)$/i.test(rec))
30567
+ return null;
30568
+ const arg0 = call.arguments.find((a) => a.position === 0);
30569
+ if (!arg0)
30570
+ return null;
30571
+ const expr = (arg0.expression ?? arg0.variable ?? "").trim();
30572
+ if (/\bresponse\.getWriter\(\)/.test(expr) || /\bresp\.getWriter\(\)/.test(expr) || /\bout\b/.test(expr) || /\bgetWriter\(\)/.test(expr)) {
30573
+ return "e.printStackTrace(response.getWriter())";
30574
+ }
30575
+ return null;
30576
+ }
30577
+ function detectResponseLeakCall(call) {
30578
+ const method = call.method_name ?? "";
30579
+ const receiver = call.receiver ?? "";
30580
+ if (!RESPONSE_SEND_METHODS.has(method))
30581
+ return null;
30582
+ if (LOGGER_RECEIVER_RE.test(receiver))
30583
+ return null;
30584
+ const recTail = receiver.split(".").pop() ?? receiver;
30585
+ const recHead = receiver.split(".")[0] ?? receiver;
30586
+ if (!RESPONSE_RECEIVER_RE.test(recTail) && !RESPONSE_RECEIVER_RE.test(recHead)) {
30587
+ if (!/(?:^|[.\s])(res|response)\.(?:status|set|header|cookie)\b/i.test(receiver)) {
30588
+ return null;
30589
+ }
30590
+ }
30591
+ for (const a of call.arguments) {
30592
+ if (argIsException(a)) {
30593
+ return `${receiver || ""}${receiver ? "." : ""}${method}(${(a.expression ?? a.variable ?? "").trim()})`;
30594
+ }
30595
+ }
30596
+ return null;
30597
+ }
30598
+ function detectPythonTracebackReturn(ctx) {
30599
+ const out2 = [];
30600
+ const lines = ctx.code.split(`
30601
+ `);
30602
+ for (let i2 = 0;i2 < lines.length; i2++) {
30603
+ const ln = lines[i2] ?? "";
30604
+ if (/\breturn\s+traceback\.format_exc\s*\(\s*\)/.test(ln) || /\breturn\s+\{[^}]*traceback\.format_exc\s*\(\s*\)[^}]*\}/.test(ln) || /\bjsonify\s*\([^)]*traceback\.format_exc\s*\(\s*\)/.test(ln)) {
30605
+ out2.push({ line: i2 + 1, api: "return traceback.format_exc()" });
30606
+ continue;
30607
+ }
30608
+ if (/\breturn\s+(?:str|repr)\s*\(\s*(?:e|err|error|exc|exception)\s*\)/.test(ln)) {
30609
+ const start2 = Math.max(0, i2 - 8);
30610
+ const end = Math.min(lines.length, i2 + 2);
30611
+ const window2 = lines.slice(start2, end).join(`
30612
+ `);
30613
+ if (/@(?:app|router|blueprint)\.(?:route|get|post|put|delete|patch)\b/.test(window2)) {
30614
+ out2.push({ line: i2 + 1, api: "return str(e) in handler" });
30615
+ }
30616
+ }
30617
+ }
30618
+ return out2;
30619
+ }
30620
+
30621
+ class InfoDisclosureStacktracePass {
30622
+ name = "info-disclosure-stacktrace";
30623
+ category = "security";
30624
+ run(ctx) {
30625
+ const { graph, language } = ctx;
30626
+ const file = graph.ir.meta.file;
30627
+ const findings = [];
30628
+ if (language === "python") {
30629
+ for (const f of detectPythonTracebackReturn(ctx)) {
30630
+ findings.push({ line: f.line, api: f.api, language });
30631
+ ctx.addFinding(this.makeFinding(file, f.line, f.api));
30632
+ }
30633
+ }
30634
+ for (const call of graph.ir.calls) {
30635
+ let api = null;
30636
+ if (language === "java") {
30637
+ api = detectJavaPrintStackTrace(call);
30638
+ if (!api)
30639
+ api = detectResponseLeakCall(call);
30640
+ } else if (language === "javascript" || language === "typescript") {
30641
+ api = detectResponseLeakCall(call);
30642
+ } else if (language === "go") {
30643
+ const method = call.method_name ?? "";
30644
+ const rec = call.receiver ?? "";
30645
+ if (rec === "http" && method === "Error") {
30646
+ const arg1 = call.arguments.find((a) => a.position === 1);
30647
+ if (argIsException(arg1))
30648
+ api = "http.Error(w, err.Error())";
30649
+ } else if (rec === "fmt" && (method === "Fprintln" || method === "Fprintf" || method === "Fprint")) {
30650
+ const arg0 = call.arguments.find((a) => a.position === 0);
30651
+ if (arg0 && /^(w|writer|resp|response)$/i.test((arg0.variable ?? arg0.expression ?? "").trim())) {
30652
+ for (const a of call.arguments) {
30653
+ if (a.position === 0)
30654
+ continue;
30655
+ if (argIsException(a)) {
30656
+ api = `fmt.${method}(w, err)`;
30657
+ break;
30658
+ }
30659
+ }
30660
+ }
30661
+ } else {
30662
+ api = detectResponseLeakCall(call);
30663
+ }
30664
+ } else if (language === "python") {
30665
+ api = detectResponseLeakCall(call);
30666
+ }
30667
+ if (!api)
30668
+ continue;
30669
+ const line = call.location.line;
30670
+ findings.push({ line, api, language });
30671
+ ctx.addFinding(this.makeFinding(file, line, api));
30672
+ }
30673
+ return { findings };
30674
+ }
30675
+ makeFinding(file, line, api) {
30676
+ return {
30677
+ id: `${this.name}-${file}-${line}`,
30678
+ pass: this.name,
30679
+ category: this.category,
30680
+ rule_id: this.name,
30681
+ cwe: "CWE-209",
30682
+ severity: "medium",
30683
+ level: "warning",
30684
+ message: `Exception detail returned to client via \`${api}\`. ` + "Leaking stack traces / exception messages reveals framework internals, " + "file paths, and class names — useful reconnaissance for an attacker.",
30685
+ file,
30686
+ line,
30687
+ fix: "Return a generic error response to the client (e.g. status 500 + a " + "request id) and log the full exception server-side via your logger " + '(e.g. `logger.error("…", e)` or `console.error(err)`).',
30688
+ evidence: { api }
30689
+ };
30690
+ }
30691
+ }
30692
+
30693
+ // ../circle-ir/dist/analysis/passes/unrestricted-file-upload-pass.js
30694
+ var UPLOAD_NAME_RE = /(?:getOriginalFilename|getSubmittedFileName|originalname|originalName|\.filename|\.Filename|FileHeader\.Filename|UploadFile)/;
30695
+ var FILE_SAFE_CALL_RE = /(?:secure_filename|FilenameUtils\.getExtension|\.lastIndexOf\(['"]\.['"]\)|ALLOWED_EXT|ALLOWED_EXTENSIONS|allowedExtensions|\bfileFilter\b|filepath\.Ext|path\.extname)/;
30696
+ function lineWindow(code, startLine, endLine) {
30697
+ const lines = code.split(`
30698
+ `);
30699
+ const s = Math.max(0, startLine - 1);
30700
+ const e = Math.min(lines.length, endLine);
30701
+ return lines.slice(s, e).join(`
30702
+ `);
30703
+ }
30704
+ function callHasUploadName(call) {
30705
+ for (const a of call.arguments) {
30706
+ const expr = (a.expression ?? a.variable ?? "").trim();
30707
+ if (UPLOAD_NAME_RE.test(expr))
30708
+ return true;
30709
+ }
30710
+ if (UPLOAD_NAME_RE.test(call.receiver ?? ""))
30711
+ return true;
30712
+ return false;
30713
+ }
30714
+
30715
+ class UnrestrictedFileUploadPass {
30716
+ name = "unrestricted-file-upload";
30717
+ category = "security";
30718
+ run(ctx) {
30719
+ const { graph, language, code } = ctx;
30720
+ const file = graph.ir.meta.file;
30721
+ const findings = [];
30722
+ const safeFunctionRanges = [];
30723
+ for (const t of graph.ir.types) {
30724
+ for (const m of t.methods) {
30725
+ const body2 = lineWindow(code, m.start_line, m.end_line);
30726
+ if (FILE_SAFE_CALL_RE.test(body2)) {
30727
+ safeFunctionRanges.push({ start: m.start_line, end: m.end_line });
30728
+ }
30729
+ }
30730
+ }
30731
+ const inSafeRange = (line) => {
30732
+ for (const r of safeFunctionRanges) {
30733
+ if (line >= r.start && line <= r.end)
30734
+ return true;
30735
+ }
30736
+ const win = lineWindow(code, Math.max(1, line - 20), line + 5);
30737
+ return FILE_SAFE_CALL_RE.test(win);
30738
+ };
30739
+ if (language === "java") {
30740
+ for (const call of graph.ir.calls) {
30741
+ const m = call.method_name ?? "";
30742
+ if (m === "transferTo" && callHasUploadName(call)) {
30743
+ if (inSafeRange(call.location.line))
30744
+ continue;
30745
+ this.emit(ctx, findings, file, call.location.line, language, "MultipartFile.transferTo(<original filename>)");
30746
+ continue;
30747
+ }
30748
+ if (m === "copy" && (call.receiver === "Files" || (call.receiver ?? "").endsWith(".Files"))) {
30749
+ if (callHasUploadName(call)) {
30750
+ if (inSafeRange(call.location.line))
30751
+ continue;
30752
+ this.emit(ctx, findings, file, call.location.line, language, "Files.copy(input, Path.of(dir, <original filename>))");
30753
+ }
30754
+ }
30755
+ }
30756
+ }
30757
+ if (language === "javascript" || language === "typescript") {
30758
+ for (const call of graph.ir.calls) {
30759
+ const m = call.method_name ?? "";
30760
+ const rec = call.receiver ?? "";
30761
+ if (m === "multer" || rec === "" && m === "multer") {
30762
+ const arg0 = call.arguments.find((a) => a.position === 0);
30763
+ const expr = (arg0?.expression ?? "").trim();
30764
+ if (/\bdest\s*:/.test(expr) && !/\bfileFilter\s*:/.test(expr)) {
30765
+ if (inSafeRange(call.location.line))
30766
+ continue;
30767
+ this.emit(ctx, findings, file, call.location.line, language, "multer({ dest }) without fileFilter");
30768
+ continue;
30769
+ }
30770
+ }
30771
+ if (rec === "fs" && (m === "writeFile" || m === "writeFileSync" || m === "appendFile")) {
30772
+ if (callHasUploadName(call) || call.arguments.some((a) => /\breq\.file(?:s)?\b/.test(a.expression ?? a.variable ?? ""))) {
30773
+ if (inSafeRange(call.location.line))
30774
+ continue;
30775
+ this.emit(ctx, findings, file, call.location.line, language, `fs.${m}(<path>, req.file.buffer)`);
30776
+ }
30777
+ }
30778
+ }
30779
+ }
30780
+ if (language === "python") {
30781
+ for (const call of graph.ir.calls) {
30782
+ const m = call.method_name ?? "";
30783
+ if (m === "save") {
30784
+ const rec = call.receiver ?? "";
30785
+ if (!/^(f|file|upload|attachment)$/i.test(rec) && rec !== "")
30786
+ continue;
30787
+ if (!callHasUploadName(call))
30788
+ continue;
30789
+ if (inSafeRange(call.location.line))
30790
+ continue;
30791
+ this.emit(ctx, findings, file, call.location.line, language, "f.save(<dir>, f.filename) without secure_filename");
30792
+ }
30793
+ }
30794
+ }
30795
+ if (language === "go") {
30796
+ for (const call of graph.ir.calls) {
30797
+ const m = call.method_name ?? "";
30798
+ const rec = call.receiver ?? "";
30799
+ if (rec === "os" && (m === "Create" || m === "OpenFile")) {
30800
+ if (callHasUploadName(call)) {
30801
+ if (inSafeRange(call.location.line))
30802
+ continue;
30803
+ this.emit(ctx, findings, file, call.location.line, language, `os.${m}(<uploaded filename>)`);
30804
+ }
30805
+ }
30806
+ if ((rec === "os" || rec === "ioutil") && m === "WriteFile") {
30807
+ if (callHasUploadName(call)) {
30808
+ if (inSafeRange(call.location.line))
30809
+ continue;
30810
+ this.emit(ctx, findings, file, call.location.line, language, `${rec}.WriteFile(<uploaded filename>, …)`);
30811
+ }
30812
+ }
30813
+ }
30814
+ }
30815
+ return { findings };
30816
+ }
30817
+ emit(ctx, findings, file, line, language, api) {
30818
+ findings.push({ line, api, language });
30819
+ ctx.addFinding({
30820
+ id: `${this.name}-${file}-${line}`,
30821
+ pass: this.name,
30822
+ category: this.category,
30823
+ rule_id: this.name,
30824
+ cwe: "CWE-434",
30825
+ severity: "high",
30826
+ level: "error",
30827
+ message: `File upload saved using untrusted name (${api}) — no extension allow-list or ` + "filename canonicalization detected. An attacker can upload a `.jsp`/`.php`/`.html` " + "file and request it back, achieving RCE or stored XSS.",
30828
+ file,
30829
+ line,
30830
+ fix: "Validate the uploaded extension against an allow-list (e.g. " + '`Set.of("png","jpg")`), then save with a sanitized filename. In Python use ' + "`werkzeug.utils.secure_filename`. In multer pass a `fileFilter`. Never " + "concatenate the upload's original filename into a save path without " + "validation.",
30831
+ evidence: { api, language }
30832
+ });
30833
+ }
30834
+ }
30835
+
30525
30836
  // ../circle-ir/dist/analysis/passes/plaintext-password-storage-pass.js
30526
30837
  function isWriteStorageCall(call, language) {
30527
30838
  const method = call.method_name ?? "";
@@ -33084,6 +33395,10 @@ async function analyze(code, filePath, language, options = {}) {
33084
33395
  pipeline.add(new XmlEntityExpansionPass);
33085
33396
  if (!disabledPasses.has("mass-assignment"))
33086
33397
  pipeline.add(new MassAssignmentPass);
33398
+ if (!disabledPasses.has("info-disclosure-stacktrace"))
33399
+ pipeline.add(new InfoDisclosureStacktracePass);
33400
+ if (!disabledPasses.has("unrestricted-file-upload"))
33401
+ pipeline.add(new UnrestrictedFileUploadPass);
33087
33402
  const { results, findings } = pipeline.run(graph, code, language, config);
33088
33403
  const sinkFilter = results.get("sink-filter");
33089
33404
  const interProc = results.get("interprocedural");
@@ -33277,7 +33592,7 @@ var colors = {
33277
33592
  };
33278
33593
 
33279
33594
  // src/version.ts
33280
- var version = "3.81.0";
33595
+ var version = "3.82.0";
33281
33596
 
33282
33597
  // src/formatters.ts
33283
33598
  var SINK_SEVERITY = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.81.0",
3
+ "version": "3.82.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",
@@ -65,7 +65,7 @@
65
65
  "registry": "https://registry.npmjs.org/"
66
66
  },
67
67
  "dependencies": {
68
- "circle-ir": "^3.81.0"
68
+ "circle-ir": "^3.82.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",