query_console 0.2.6 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a47be0c8cd2c38f02425d2bbdd1a077c8dc2ca7903a336cf05bbc5d39042e86
4
- data.tar.gz: e7f5487b5ea34bc21280d87181a5b74afcc4b12f9f592894801838bc84d82686
3
+ metadata.gz: 19231f63422842fbe797464f96c6208d308ec868d8cb19872cceabd32a132c1a
4
+ data.tar.gz: edeedcb088b35135eabb6b4a445b315832ce0a500424f7718d84300dcfb3e08d
5
5
  SHA512:
6
- metadata.gz: b5987fca8610f57bc4f83eee25adb96bd073a72e2dbe34fc7e4c989d11281f89812c9d2d8ced545ee22a2f501e800ad2292e429b889c4194aedf5eb6ffa13e78
7
- data.tar.gz: dc2f080870957fac2ac78a764036e843b3af76b6f2c85ce3385a14e2b9dc563312f7dac1defa1a757d36a6db3bf0633230b338a10c31b3f0f9326710cd47b58a
6
+ metadata.gz: be36ac0c732088e1fc1a683b435dc50597791fe5a46fe917c116de1464f34aa378f70701a09095a52bd677ee6b0b94f5fe758be4e302a1af0ae6d3697f5337a1
7
+ data.tar.gz: 9f5610145cb80d7557c5eb86f1655fb07483397e05616667a450a0b575ffd3c082728795b54c0db189a58dfe12bc46aeb46cdfa3d51ce33c20bbc6dd2321b513
@@ -16,6 +16,27 @@ module QueryConsole
16
16
  return
17
17
  end
18
18
 
19
+ # SECURITY FIX: Server-side DML confirmation check
20
+ # Validate DML confirmation before execution
21
+ config = QueryConsole.configuration
22
+ if config.enable_dml
23
+ # Quick check if SQL contains DML keywords
24
+ normalized_sql = sql.strip.downcase
25
+ if normalized_sql.match?(/\A(insert|update|delete|merge)\b/)
26
+ # This is a DML query - require confirmation
27
+ unless params[:dml_confirmed] == 'true'
28
+ @result = Runner::QueryResult.new(
29
+ error: "DML query execution requires user confirmation. Please confirm the operation to proceed."
30
+ )
31
+ respond_to do |format|
32
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("query-results", partial: "results", locals: { result: @result, is_dml: false }) }
33
+ format.html { render :_results, layout: false }
34
+ end
35
+ return
36
+ end
37
+ end
38
+ end
39
+
19
40
  # Execute the query
20
41
  runner = Runner.new(sql)
21
42
  @result = runner.execute
@@ -39,17 +39,33 @@ module QueryConsole
39
39
  end
40
40
 
41
41
  def self.is_dml_query?(sql)
42
- sql.to_s.strip.downcase.match?(/\A(insert|update|delete|merge)\b/)
42
+ normalized = sql.to_s.strip.downcase
43
+ # Check if it's a top-level DML query
44
+ is_top_level = normalized.match?(/\A(insert|update|delete|merge)\b/)
45
+ # SECURITY FIX: Also check for DML anywhere in the query (subqueries)
46
+ has_dml_anywhere = normalized.match?(/\b(insert|update|delete|merge)\b/)
47
+
48
+ # Return true if DML detected anywhere (for audit purposes)
49
+ is_top_level || has_dml_anywhere
43
50
  end
44
51
 
45
52
  def self.determine_query_type(sql)
46
- case sql.to_s.strip.downcase
53
+ normalized = sql.to_s.strip.downcase
54
+
55
+ # SECURITY FIX: Check for DML anywhere in query, not just at start
56
+ # This ensures subquery DML is properly logged
57
+ if normalized.match?(/\b(insert|update|delete|merge)\b/)
58
+ # Determine which DML operation (prefer top-level, but detect any)
59
+ return "INSERT" if normalized.match?(/\binsert\b/)
60
+ return "UPDATE" if normalized.match?(/\bupdate\b/)
61
+ return "DELETE" if normalized.match?(/\bdelete\b/)
62
+ return "MERGE" if normalized.match?(/\bmerge\b/)
63
+ end
64
+
65
+ # If no DML, check for SELECT/WITH
66
+ case normalized
47
67
  when /\Aselect\b/ then "SELECT"
48
68
  when /\Awith\b/ then "WITH"
49
- when /\Ainsert\b/ then "INSERT"
50
- when /\Aupdate\b/ then "UPDATE"
51
- when /\Adelete\b/ then "DELETE"
52
- when /\Amerge\b/ then "MERGE"
53
69
  else "UNKNOWN"
54
70
  end
55
71
  end
@@ -67,11 +67,24 @@ module QueryConsole
67
67
  # Check for forbidden keywords
68
68
  normalized_query = sanitized.downcase
69
69
 
70
- # Filter forbidden keywords based on DML enablement
71
- effective_forbidden = if @config.enable_dml
72
- @config.forbidden_keywords.reject { |kw| dml_keywords.include?(kw) || kw == 'replace' || kw == 'into' }
70
+ # SECURITY FIX: When DML is enabled, we still need to prevent DML in subqueries
71
+ # Only allow DML at the top level (start of query)
72
+ if @config.enable_dml
73
+ # Check if this is a top-level DML query
74
+ is_top_level_dml = normalized_start.match?(/\A(insert|update|delete|merge)\b/)
75
+
76
+ # If DML keywords appear anywhere else (subqueries, CTEs), block them
77
+ if !is_top_level_dml && normalized_query.match?(/\b(insert|update|delete|merge)\b/)
78
+ return ValidationResult.new(
79
+ valid: false,
80
+ error: "DML keywords (INSERT, UPDATE, DELETE, MERGE) are not allowed in subqueries or WITH clauses"
81
+ )
82
+ end
83
+
84
+ # Filter forbidden keywords - only remove DML from forbidden list for top-level queries
85
+ effective_forbidden = @config.forbidden_keywords.reject { |kw| dml_keywords.include?(kw) || kw == 'replace' || kw == 'into' }
73
86
  else
74
- @config.forbidden_keywords
87
+ effective_forbidden = @config.forbidden_keywords
75
88
  end
76
89
 
77
90
  forbidden = effective_forbidden.find do |keyword|
@@ -86,7 +99,7 @@ module QueryConsole
86
99
  )
87
100
  end
88
101
 
89
- # Detect if this is a DML query
102
+ # Detect if this is a DML query (top-level only)
90
103
  is_dml_query = sanitized.downcase.match?(/\A(insert|update|delete|merge)\b/)
91
104
 
92
105
  ValidationResult.new(valid: true, sanitized_sql: sanitized, is_dml: is_dml_query)
@@ -597,10 +597,19 @@
597
597
  form.method = 'POST'
598
598
  form.action = '<%= run_path %>'
599
599
  form.setAttribute('data-turbo-frame', 'query-results')
600
- form.innerHTML = `
600
+
601
+ // SECURITY FIX: Add dml_confirmed parameter for server-side verification
602
+ let formHTML = `
601
603
  <input type="hidden" name="sql" value="${this.escapeHtml(sql)}">
602
604
  <input type="hidden" name="authenticity_token" value="${document.querySelector('meta[name=csrf-token]').content}">
603
605
  `
606
+
607
+ // If DML query was confirmed, add confirmation parameter
608
+ if (this.isDmlQuery(sql)) {
609
+ formHTML += `<input type="hidden" name="dml_confirmed" value="true">`
610
+ }
611
+
612
+ form.innerHTML = formHTML
604
613
  document.body.appendChild(form)
605
614
  form.requestSubmit()
606
615
  document.body.removeChild(form)
@@ -790,7 +799,7 @@
790
799
  history.map((item, index) => `
791
800
  <li data-action="click->history#load" data-index="${index}">
792
801
  <div style="font-size: 12px; color: #6c757d;">${new Date(item.timestamp).toLocaleString()}</div>
793
- <div style="font-size: 13px; margin-top: 4px;">${this.truncate(item.sql, 100)}</div>
802
+ <div style="font-size: 13px; margin-top: 4px;">${this.escapeHtml(this.truncate(item.sql, 100))}</div>
794
803
  </li>
795
804
  `).join('') :
796
805
  '<li style="color: #6c757d; text-align: center; padding: 20px;">No query history</li>'
@@ -830,6 +839,13 @@
830
839
  localStorage.setItem(this.storageKeyValue, JSON.stringify(history))
831
840
  }
832
841
 
842
+ // SECURITY FIX: Escape HTML to prevent XSS
843
+ escapeHtml(text) {
844
+ const div = document.createElement('div')
845
+ div.textContent = text
846
+ return div.innerHTML
847
+ }
848
+
833
849
  truncate(str, length) {
834
850
  return str.length > length ? str.substring(0, length) + '...' : str
835
851
  }
@@ -1,3 +1,3 @@
1
1
  module QueryConsole
2
- VERSION = "0.2.6"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: query_console
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johnson Gnanasekar