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 +4 -4
- data/app/controllers/query_console/queries_controller.rb +21 -0
- data/app/services/query_console/audit_logger.rb +22 -6
- data/app/services/query_console/sql_validator.rb +18 -5
- data/app/views/query_console/queries/new.html.erb +18 -2
- data/lib/query_console/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19231f63422842fbe797464f96c6208d308ec868d8cb19872cceabd32a132c1a
|
|
4
|
+
data.tar.gz: edeedcb088b35135eabb6b4a445b315832ce0a500424f7718d84300dcfb3e08d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
}
|