dbviewer 0.9.1 โ 0.9.2
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/README.md +65 -101
- data/app/controllers/concerns/dbviewer/database_operations/query_operations.rb +43 -5
- data/lib/dbviewer/configuration.rb +24 -0
- data/lib/dbviewer/query/executor.rb +22 -1
- data/lib/dbviewer/query/logger.rb +36 -0
- data/lib/dbviewer/validator/sql/threat_detector.rb +43 -1
- data/lib/dbviewer/validator/sql/validation_config.rb +27 -2
- data/lib/dbviewer/validator/sql.rb +59 -7
- data/lib/dbviewer/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: 73a576f4a8f66beb4c9320015567379b28baf8f11d857b4e5b69ff7372ec0977
|
4
|
+
data.tar.gz: f408948d8b866d8a223035555d31b9b5131f7d3c21ae4d2fe3864dfbd5670d75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c973bd58d1b0e0005bcf0591d72d70d58ba917c2c4e39f1678a77011f4832e25e53d22d0153fbded40612e0d8e1bc43ef75271e64fc20f18489920e4299a8fa2
|
7
|
+
data.tar.gz: a464fd32e361fa3e5eab3917f72b9b1e08e98f0887e5cc396a685a0624ff71c7a74998887ec38f0ed71432535546b354c1086618269c48781806b17ea73f92fd
|
data/README.md
CHANGED
@@ -9,15 +9,16 @@ It's designed for development, debugging, and database analysis, offering a clea
|
|
9
9
|
|
10
10
|
## โจ Features
|
11
11
|
|
12
|
-
- **Dashboard**
|
13
|
-
- **Table Overview**
|
14
|
-
- **Detailed Schema Information**
|
15
|
-
- **Entity Relationship Diagram (ERD)**
|
16
|
-
- **Data Browsing**
|
17
|
-
- **SQL Queries**
|
18
|
-
- **Multiple Database Connections**
|
19
|
-
- **PII Data Masking** -
|
20
|
-
- **
|
12
|
+
- **Dashboard** - Comprehensive database overview with statistics
|
13
|
+
- **Table Overview** - Complete table listing with metadata
|
14
|
+
- **Detailed Schema Information** - Column details, indexes, and constraints
|
15
|
+
- **Entity Relationship Diagram (ERD)** - Interactive database schema visualization
|
16
|
+
- **Data Browsing** - Paginated record viewing with search and filtering
|
17
|
+
- **SQL Queries** - Safe SQL query execution with validation
|
18
|
+
- **Multiple Database Connections** - Support for multiple database sources
|
19
|
+
- **PII Data Masking** - Configurable masking for sensitive data
|
20
|
+
- **Access Control** - Table and column-level access restrictions
|
21
|
+
- **Query Logging** - SQL query monitoring and performance analysis
|
21
22
|
|
22
23
|
## ๐งช Demo Application
|
23
24
|
|
@@ -126,34 +127,6 @@ You can also create this file manually if you prefer.
|
|
126
127
|
|
127
128
|
The configuration is accessed through `Dbviewer.configuration` throughout the codebase. You can also access it via `Dbviewer.config` which is an alias for backward compatibility.
|
128
129
|
|
129
|
-
### Disabling DBViewer Completely
|
130
|
-
|
131
|
-
You can completely disable DBViewer access by setting the `disabled` configuration option to `true`. When disabled, all DBViewer routes will return 404 (Not Found) responses:
|
132
|
-
|
133
|
-
```ruby
|
134
|
-
# config/initializers/dbviewer.rb
|
135
|
-
Dbviewer.configure do |config|
|
136
|
-
# Completely disable DBViewer in production
|
137
|
-
config.disabled = Rails.env.production?
|
138
|
-
|
139
|
-
# Or disable unconditionally
|
140
|
-
# config.disabled = true
|
141
|
-
end
|
142
|
-
```
|
143
|
-
|
144
|
-
This is useful for:
|
145
|
-
|
146
|
-
- **Production environments** where you want to completely disable access to database viewing tools
|
147
|
-
- **Security compliance** where database admin tools must be disabled in certain environments
|
148
|
-
- **Performance** where you want to eliminate any potential overhead from DBViewer routes
|
149
|
-
|
150
|
-
When disabled:
|
151
|
-
|
152
|
-
- All DBViewer routes return 404 (Not Found) responses
|
153
|
-
- No database connections are validated
|
154
|
-
- No DBViewer middleware or concerns are executed
|
155
|
-
- The application behaves as if DBViewer was never mounted
|
156
|
-
|
157
130
|
### Multiple Database Connections
|
158
131
|
|
159
132
|
DBViewer supports working with multiple database connections in your application. This is useful for applications that connect to multiple databases or use different connection pools.
|
@@ -214,23 +187,32 @@ config.query_logging_mode = :file # Store queries in a log file
|
|
214
187
|
config.query_log_path = "log/dbviewer.log" # Path where query log file will be stored
|
215
188
|
```
|
216
189
|
|
217
|
-
The file format uses one JSON entry per line, making it easy to analyze with standard tools.
|
190
|
+
The file format uses one JSON entry per line, making it easy to analyze with standard tools.
|
191
|
+
|
192
|
+
**Note**: Query logging is automatically disabled in non-development environments for performance.
|
218
193
|
|
219
194
|
## ๐ Security Features
|
220
195
|
|
221
|
-
DBViewer includes
|
196
|
+
DBViewer includes comprehensive security features to protect your database:
|
197
|
+
|
198
|
+
### Core Security
|
222
199
|
|
223
200
|
- **Read-only Mode**: Only SELECT queries are allowed; all data modification operations are blocked
|
224
201
|
- **SQL Validation**: Prevents potentially harmful operations with comprehensive validation
|
225
202
|
- **Query Limits**: Automatic LIMIT clause added to prevent excessive data retrieval
|
226
203
|
- **Pattern Detection**: Detection of SQL injection patterns and suspicious constructs
|
227
204
|
- **Error Handling**: Informative error messages without exposing sensitive information
|
228
|
-
|
229
|
-
|
205
|
+
|
206
|
+
### Authentication & Access Control
|
207
|
+
|
208
|
+
- **HTTP Basic Authentication**: Protect access with username and password
|
209
|
+
- **Table-Level Access Control**: Whitelist/blacklist specific tables
|
210
|
+
- **Column-Level Blocking**: Hide sensitive columns from display
|
211
|
+
- **Complete Disabling**: Fully disable DBViewer in production environments
|
230
212
|
|
231
213
|
### Basic Authentication
|
232
214
|
|
233
|
-
|
215
|
+
Enable HTTP Basic Authentication to secure access:
|
234
216
|
|
235
217
|
```ruby
|
236
218
|
Dbviewer.configure do |config|
|
@@ -242,65 +224,43 @@ end
|
|
242
224
|
```
|
243
225
|
|
244
226
|
When credentials are provided, all DBViewer routes will be protected by HTTP Basic Authentication.
|
245
|
-
Without valid credentials, users will be prompted for a username and password before they can access any DBViewer page.
|
246
227
|
|
247
|
-
### Complete Disabling
|
228
|
+
### Complete Disabling for Production
|
248
229
|
|
249
|
-
For maximum security in production environments,
|
230
|
+
For maximum security in production environments, completely disable DBViewer:
|
250
231
|
|
251
232
|
```ruby
|
252
233
|
Dbviewer.configure do |config|
|
253
234
|
# Completely disable DBViewer in production
|
254
235
|
config.disabled = Rails.env.production?
|
236
|
+
|
237
|
+
# Or disable unconditionally
|
238
|
+
# config.disabled = true
|
255
239
|
end
|
256
240
|
```
|
257
241
|
|
258
|
-
When disabled, all DBViewer routes return 404 responses, making it appear as if the tool was never installed. This is the recommended approach for production systems
|
242
|
+
When disabled, all DBViewer routes return 404 responses, making it appear as if the tool was never installed. This is the recommended approach for production systems.
|
243
|
+
|
244
|
+
โ ๏ธ **Security Warning**: This engine provides direct access to your database contents. In production:
|
245
|
+
|
246
|
+
- Use long, randomly generated passwords (e.g., `SecureRandom.hex(16)`)
|
247
|
+
- Access DBViewer over HTTPS connections only
|
248
|
+
- Limit access to trusted administrators only
|
249
|
+
- Consider completely disabling in production environments
|
259
250
|
|
260
251
|
### ๐ PII Data Masking
|
261
252
|
|
262
253
|
DBViewer includes built-in support for masking Personally Identifiable Information (PII) to protect sensitive data while allowing developers to browse database contents.
|
263
254
|
|
264
|
-
|
265
|
-
|
266
|
-
Configure PII masking in your Rails initializer (e.g., `config/initializers/dbviewer.rb`):
|
255
|
+
Enable PII masking in your Rails initializer:
|
267
256
|
|
268
257
|
```ruby
|
269
|
-
# Enable PII masking (enabled by default)
|
270
258
|
Dbviewer.configure do |config|
|
271
259
|
config.enable_pii_masking = true
|
272
260
|
end
|
273
|
-
|
274
|
-
# Define masking rules
|
275
|
-
Dbviewer.configure_pii do |pii|
|
276
|
-
# Built-in masking types
|
277
|
-
pii.mask 'users.email', with: :email # john@example.com โ jo***@example.com
|
278
|
-
pii.mask 'users.phone', with: :phone # +1234567890 โ +1***90
|
279
|
-
pii.mask 'users.ssn', with: :ssn # 123456789 โ ***-**-6789
|
280
|
-
pii.mask 'payments.card_number', with: :credit_card # 1234567890123456 โ ****-****-****-3456
|
281
|
-
pii.mask 'users.api_key', with: :full_redact # any_value โ ***REDACTED***
|
282
|
-
|
283
|
-
# Custom masking with lambda
|
284
|
-
pii.mask 'users.salary', with: ->(value) { value ? '$***,***' : value }
|
285
|
-
|
286
|
-
# Define reusable custom masks
|
287
|
-
pii.custom_mask :ip_mask, ->(value) {
|
288
|
-
return value if value.nil?
|
289
|
-
parts = value.split('.')
|
290
|
-
"#{parts[0]}.#{parts[1]}.***.***.***"
|
291
|
-
}
|
292
|
-
pii.mask 'logs.ip_address', with: :ip_mask
|
293
|
-
end
|
294
261
|
```
|
295
262
|
|
296
|
-
|
297
|
-
|
298
|
-
- **`:email`** - Masks email addresses while preserving domain
|
299
|
-
- **`:phone`** - Masks phone numbers keeping first and last digits
|
300
|
-
- **`:ssn`** - Masks Social Security Numbers showing only last 4 digits
|
301
|
-
- **`:credit_card`** - Masks credit card numbers showing only last 4 digits
|
302
|
-
- **`:full_redact`** - Completely redacts the value
|
303
|
-
- **`:partial`** - Partial masking (default behavior)
|
263
|
+
For complete setup instructions, built-in masking types, and advanced configuration examples, see [PII_MASKING.md](docs/PII_MASKING.md).
|
304
264
|
|
305
265
|
### Table and Column Access Control
|
306
266
|
|
@@ -310,35 +270,39 @@ DBViewer includes granular access control features to restrict access to specifi
|
|
310
270
|
|
311
271
|
DBViewer supports three access control modes:
|
312
272
|
|
313
|
-
- **`:none`** (default) - All tables are accessible
|
273
|
+
- **`:none`** (default) - All tables are accessible
|
314
274
|
- **`:whitelist`** - Only explicitly allowed tables are accessible (most secure)
|
315
275
|
- **`:blacklist`** - All tables except explicitly blocked ones are accessible
|
316
276
|
|
317
|
-
####
|
318
|
-
|
319
|
-
Whitelist mode is the most secure approach, where only explicitly allowed tables can be accessed:
|
277
|
+
#### Configuration Examples
|
320
278
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
279
|
+
```ruby
|
280
|
+
# config/initializers/dbviewer.rb
|
281
|
+
Dbviewer.configure do |config|
|
282
|
+
# Whitelist mode (recommended for production)
|
283
|
+
config.access_control_mode = :whitelist
|
284
|
+
config.allowed_tables = ['users', 'orders', 'products', 'categories']
|
285
|
+
|
286
|
+
# OR blacklist mode
|
287
|
+
# config.access_control_mode = :blacklist
|
288
|
+
# config.blocked_tables = ['admin_users', 'sensitive_data', 'audit_logs']
|
289
|
+
|
290
|
+
# Hide sensitive columns from specific tables
|
291
|
+
config.blocked_columns = {
|
292
|
+
'users' => ['password_digest', 'api_key', 'secret_token'],
|
293
|
+
'orders' => ['internal_notes', 'admin_comments']
|
294
|
+
}
|
295
|
+
end
|
327
296
|
```
|
328
297
|
|
329
|
-
|
298
|
+
When access control is enabled, DBViewer will:
|
330
299
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
โ ๏ธ **Warning**: This engine provides direct access to your database contents, which contains sensitive information. Always protect it with HTTP Basic Authentication by configuring strong credentials as shown above.
|
300
|
+
- Validate all table access in UI, API endpoints, and Entity Relationship Diagrams
|
301
|
+
- Filter SQL queries to prevent unauthorized table access
|
302
|
+
- Show only accessible tables and their relationships in ERDs
|
303
|
+
- Provide informative error messages for access violations
|
336
304
|
|
337
|
-
|
338
|
-
|
339
|
-
- You use long, randomly generated passwords (e.g., with `SecureRandom.hex(16)`)
|
340
|
-
- You access DBViewer over HTTPS connections only
|
341
|
-
- Access is limited to trusted administrators only
|
305
|
+
For detailed PII masking documentation, see [PII_MASKING.md](docs/PII_MASKING.md).
|
342
306
|
|
343
307
|
## ๐ Updating DBViewer
|
344
308
|
|
@@ -355,7 +319,7 @@ The simplest way to update is using Bundler:
|
|
355
319
|
gem "dbviewer"
|
356
320
|
|
357
321
|
# Or specify a version
|
358
|
-
gem "dbviewer", "~> 0.
|
322
|
+
gem "dbviewer", "~> 0.9.0"
|
359
323
|
```
|
360
324
|
|
361
325
|
- Run bundle update:
|
@@ -5,20 +5,23 @@ module Dbviewer
|
|
5
5
|
|
6
6
|
# Prepare the SQL query - either from params or default
|
7
7
|
def prepare_query(table_name, query)
|
8
|
-
|
8
|
+
# Sanitize and validate input
|
9
|
+
sanitized_query = sanitize_query_input(query)
|
10
|
+
final_query = sanitized_query.present? ? sanitized_query.to_s : default_query(table_name)
|
9
11
|
|
10
12
|
# Validate query for security
|
11
|
-
unless ::Dbviewer::Validator::Sql.safe_query?(
|
12
|
-
|
13
|
+
unless ::Dbviewer::Validator::Sql.safe_query?(final_query)
|
14
|
+
log_unsafe_query_attempt(final_query)
|
15
|
+
final_query = default_query(table_name)
|
13
16
|
flash.now[:warning] = "Only SELECT queries are allowed. Your query contained potentially unsafe operations. Using default query instead."
|
14
17
|
end
|
15
18
|
|
16
|
-
|
19
|
+
final_query
|
17
20
|
end
|
18
21
|
|
19
22
|
# Execute the prepared SQL query
|
20
23
|
def execute_query(query)
|
21
|
-
database_manager.execute_query(
|
24
|
+
database_manager.execute_query(query)
|
22
25
|
end
|
23
26
|
|
24
27
|
def default_query(table_name)
|
@@ -32,6 +35,41 @@ module Dbviewer
|
|
32
35
|
def safe_quote_table_name(table_name)
|
33
36
|
database_manager.connection.quote_table_name(table_name) rescue table_name.to_s
|
34
37
|
end
|
38
|
+
|
39
|
+
# Sanitize query input to prevent basic injection attempts
|
40
|
+
def sanitize_query_input(query)
|
41
|
+
return nil if query.nil?
|
42
|
+
|
43
|
+
# Convert to string and strip whitespace
|
44
|
+
sanitized = query.to_s.strip
|
45
|
+
|
46
|
+
# Remove any null bytes that could be used to bypass security
|
47
|
+
sanitized = sanitized.gsub(/\x00/, "")
|
48
|
+
|
49
|
+
# Limit the query length as an additional safety measure
|
50
|
+
max_length = 10000
|
51
|
+
sanitized = sanitized.truncate(max_length) if sanitized.length > max_length
|
52
|
+
|
53
|
+
sanitized
|
54
|
+
end
|
55
|
+
|
56
|
+
# Log unsafe query attempts for security monitoring
|
57
|
+
def log_unsafe_query_attempt(query)
|
58
|
+
Rails.logger.warn("[DBViewer][Security] Unsafe query blocked: #{query.truncate(200)}") if defined?(Rails)
|
59
|
+
|
60
|
+
# Log to security monitoring system if available
|
61
|
+
if defined?(::Dbviewer::Query::Logger)
|
62
|
+
::Dbviewer::Query::Logger.log_security_event(
|
63
|
+
event_type: "unsafe_query_blocked",
|
64
|
+
query_type: "user_query",
|
65
|
+
sql: query,
|
66
|
+
timestamp: Time.current
|
67
|
+
)
|
68
|
+
end
|
69
|
+
rescue => e
|
70
|
+
# Don't let logging errors break the application
|
71
|
+
puts "[DBViewer] Security logging error: #{e.message}"
|
72
|
+
end
|
35
73
|
end
|
36
74
|
end
|
37
75
|
end
|
@@ -100,6 +100,23 @@ module Dbviewer
|
|
100
100
|
# :none - all tables accessible (current behavior)
|
101
101
|
attr_accessor :access_control_mode
|
102
102
|
|
103
|
+
# Security-related configuration options
|
104
|
+
|
105
|
+
# Enable comprehensive security logging for all database operations
|
106
|
+
attr_accessor :log_queries
|
107
|
+
|
108
|
+
# Log security threats and blocked queries
|
109
|
+
attr_accessor :log_security_events
|
110
|
+
|
111
|
+
# Enhanced SQL injection detection patterns
|
112
|
+
attr_accessor :enhanced_sql_protection
|
113
|
+
|
114
|
+
# Enforce parameterized queries when possible
|
115
|
+
attr_accessor :enforce_parameterized_queries
|
116
|
+
|
117
|
+
# Maximum number of security events to keep in memory
|
118
|
+
attr_accessor :max_security_events
|
119
|
+
|
103
120
|
def initialize
|
104
121
|
@per_page_options = [ 10, 20, 50, 100 ]
|
105
122
|
@default_per_page = 20
|
@@ -132,6 +149,13 @@ module Dbviewer
|
|
132
149
|
@blocked_tables = []
|
133
150
|
@blocked_columns = {}
|
134
151
|
@access_control_mode = :none # Default to current behavior
|
152
|
+
|
153
|
+
# Initialize security settings
|
154
|
+
@log_queries = true
|
155
|
+
@log_security_events = true
|
156
|
+
@enhanced_sql_protection = true
|
157
|
+
@enforce_parameterized_queries = false
|
158
|
+
@max_security_events = 1000
|
135
159
|
end
|
136
160
|
end
|
137
161
|
end
|
@@ -15,6 +15,7 @@ module Dbviewer
|
|
15
15
|
# @return [ActiveRecord::Result] Result set with columns and rows
|
16
16
|
# @raise [StandardError] If the query is invalid or unsafe
|
17
17
|
def execute_query(sql)
|
18
|
+
log_query_execution(sql, "query")
|
18
19
|
exec_query(normalize_sql(sql))
|
19
20
|
end
|
20
21
|
|
@@ -23,7 +24,9 @@ module Dbviewer
|
|
23
24
|
# @return [ActiveRecord::Result] Result set with the PRAGMA value
|
24
25
|
# @raise [StandardError] If the query is invalid or cannot be executed
|
25
26
|
def execute_sqlite_pragma(pragma)
|
26
|
-
|
27
|
+
pragma_sql = "PRAGMA #{pragma}"
|
28
|
+
log_query_execution(pragma_sql, "pragma")
|
29
|
+
exec_query(pragma_sql)
|
27
30
|
end
|
28
31
|
|
29
32
|
private
|
@@ -38,6 +41,24 @@ module Dbviewer
|
|
38
41
|
normalized_sql = "#{normalized_sql} LIMIT #{max_records}" unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
|
39
42
|
normalized_sql
|
40
43
|
end
|
44
|
+
|
45
|
+
# Log query execution for security monitoring
|
46
|
+
# @param sql [String] The SQL query being executed
|
47
|
+
# @param query_type [String] Type of query (query, pragma, etc.)
|
48
|
+
def log_query_execution(sql, query_type)
|
49
|
+
return unless should_log_queries?
|
50
|
+
|
51
|
+
::Dbviewer::Query::Logger.log_security_event(
|
52
|
+
event_type: "query_execution",
|
53
|
+
query_type: query_type,
|
54
|
+
sql: sql,
|
55
|
+
timestamp: Time.current
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def should_log_queries?
|
60
|
+
@config.respond_to?(:log_queries) ? @config.log_queries : true
|
61
|
+
end
|
41
62
|
end
|
42
63
|
end
|
43
64
|
end
|
@@ -72,6 +72,22 @@ module Dbviewer
|
|
72
72
|
Analyzer.generate_stats(queries)
|
73
73
|
end
|
74
74
|
|
75
|
+
# Add a security event to the logger
|
76
|
+
def add_security_event(event)
|
77
|
+
# Store security events separately for analysis
|
78
|
+
@security_events ||= []
|
79
|
+
@security_events << event
|
80
|
+
|
81
|
+
# Keep only the last 1000 security events to prevent memory issues
|
82
|
+
@security_events = @security_events.last(1000) if @security_events.size > 1000
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get recent security events
|
86
|
+
def recent_security_events(limit: 100)
|
87
|
+
@security_events ||= []
|
88
|
+
@security_events.last(limit)
|
89
|
+
end
|
90
|
+
|
75
91
|
class << self
|
76
92
|
extend Forwardable
|
77
93
|
|
@@ -87,6 +103,26 @@ module Dbviewer
|
|
87
103
|
query_logging_mode: query_logging_mode
|
88
104
|
)
|
89
105
|
end
|
106
|
+
|
107
|
+
# Log security events for monitoring and auditing
|
108
|
+
# @param event_type [String] Type of security event
|
109
|
+
# @param query_type [String] Type of query being executed
|
110
|
+
# @param sql [String] The SQL query
|
111
|
+
# @param timestamp [Time] When the event occurred
|
112
|
+
def log_security_event(event_type:, query_type:, sql:, timestamp:)
|
113
|
+
# Log to Rails logger with security prefix for easy filtering
|
114
|
+
Rails.logger.info("[DBViewer][Security] #{event_type.upcase}: #{query_type} - #{sql.truncate(200)}")
|
115
|
+
|
116
|
+
# Also store in memory for potential analysis
|
117
|
+
instance.add_security_event({
|
118
|
+
event_type: event_type,
|
119
|
+
query_type: query_type,
|
120
|
+
sql: sql,
|
121
|
+
timestamp: timestamp,
|
122
|
+
request_id: ActiveSupport::Notifications.instrumenter.id,
|
123
|
+
thread_id: Thread.current.object_id.to_s
|
124
|
+
})
|
125
|
+
end
|
90
126
|
end
|
91
127
|
|
92
128
|
private
|
@@ -22,6 +22,7 @@ module Dbviewer
|
|
22
22
|
return true if has_string_concatenation?(sql)
|
23
23
|
return true if has_excessive_quotes?(sql)
|
24
24
|
return true if has_hex_encoding?(sql)
|
25
|
+
return true if has_additional_suspicious_patterns?(sql)
|
25
26
|
|
26
27
|
false
|
27
28
|
end
|
@@ -33,7 +34,15 @@ module Dbviewer
|
|
33
34
|
# @param sql [String] Raw SQL query (before normalization)
|
34
35
|
# @return [Boolean] true if injection patterns found, false otherwise
|
35
36
|
def has_injection_patterns?(sql)
|
36
|
-
ValidationConfig::INJECTION_PATTERNS
|
37
|
+
patterns_to_check = ValidationConfig::INJECTION_PATTERNS
|
38
|
+
|
39
|
+
# Filter out enhanced patterns if enhanced protection is disabled
|
40
|
+
unless enhanced_protection_enabled?
|
41
|
+
enhanced_patterns = [ :information_schema, :mysql_user, :pg_user ]
|
42
|
+
patterns_to_check = patterns_to_check.reject { |name, _| enhanced_patterns.include?(name) }
|
43
|
+
end
|
44
|
+
|
45
|
+
patterns_to_check.any? do |_name, pattern|
|
37
46
|
sql =~ pattern
|
38
47
|
end
|
39
48
|
end
|
@@ -89,6 +98,16 @@ module Dbviewer
|
|
89
98
|
|
90
99
|
private
|
91
100
|
|
101
|
+
# Check if enhanced SQL protection is enabled
|
102
|
+
def enhanced_protection_enabled?
|
103
|
+
if defined?(Dbviewer) && Dbviewer.respond_to?(:configuration) &&
|
104
|
+
Dbviewer.configuration.respond_to?(:enhanced_sql_protection)
|
105
|
+
Dbviewer.configuration.enhanced_sql_protection
|
106
|
+
else
|
107
|
+
true # Default to enabled for security
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
92
111
|
# Check for comment injection attempts
|
93
112
|
# Comments can be used to hide malicious SQL code
|
94
113
|
#
|
@@ -127,6 +146,29 @@ module Dbviewer
|
|
127
146
|
def has_hex_encoding?(sql)
|
128
147
|
ValidationConfig::SUSPICIOUS_PATTERNS[:hex_encoding] =~ sql
|
129
148
|
end
|
149
|
+
|
150
|
+
# Check for additional suspicious patterns
|
151
|
+
# This method checks for newer and more sophisticated attack patterns
|
152
|
+
#
|
153
|
+
# @param sql [String] Raw SQL query
|
154
|
+
# @return [Boolean] true if additional suspicious patterns detected
|
155
|
+
def has_additional_suspicious_patterns?(sql)
|
156
|
+
additional_patterns = [
|
157
|
+
:char_function, :ascii_function, :substring_injection, :length_functions,
|
158
|
+
:conditional_comments, :encoded_spaces, :multiple_unions, :nested_selects,
|
159
|
+
:script_tags, :php_tags, :null_byte, :excessive_parentheses
|
160
|
+
]
|
161
|
+
|
162
|
+
# Only check enhanced patterns if enhanced protection is enabled
|
163
|
+
unless enhanced_protection_enabled?
|
164
|
+
enhanced_suspicious_patterns = [ :ascii_function, :substring_injection, :length_functions, :char_function ]
|
165
|
+
additional_patterns = additional_patterns - enhanced_suspicious_patterns
|
166
|
+
end
|
167
|
+
|
168
|
+
additional_patterns.any? do |pattern_name|
|
169
|
+
ValidationConfig::SUSPICIOUS_PATTERNS[pattern_name] =~ sql
|
170
|
+
end
|
171
|
+
end
|
130
172
|
end
|
131
173
|
end
|
132
174
|
end
|
@@ -33,7 +33,20 @@ module Dbviewer
|
|
33
33
|
SUSPICIOUS_PATTERNS = {
|
34
34
|
comment_injection: /\s+--|\/\*/,
|
35
35
|
string_concatenation: /\|\||CONCAT\s*\(/i,
|
36
|
-
hex_encoding: /0x[0-9a-f]{16,}/i
|
36
|
+
hex_encoding: /0x[0-9a-f]{16,}/i,
|
37
|
+
# Additional suspicious patterns
|
38
|
+
char_function: /\bCHAR\s*\(\s*\d+(\s*,\s*\d+){4,}/i, # Only flag when many parameters like CHAR(65,68,77,73,78)
|
39
|
+
ascii_function: /\bASCII\s*\(/i,
|
40
|
+
substring_injection: /\bSUBSTRING\s*\(\s*(@@|version|user|database)/i, # Only when extracting system info
|
41
|
+
length_functions: /\b(LENGTH|LEN|CHAR_LENGTH)\s*\(\s*(@@|version|user|database)/i, # Only on system functions
|
42
|
+
conditional_comments: /\/\*!\d+/,
|
43
|
+
encoded_spaces: /%20|%09|%0a|%0d/i,
|
44
|
+
multiple_unions: /\bUNION\b.*\bUNION\b/i,
|
45
|
+
nested_selects: /\bSELECT\b.*\bSELECT\b.*\bSELECT\b/i,
|
46
|
+
script_tags: /<script|<\/script>/i,
|
47
|
+
php_tags: /<\?php|<\?=/i,
|
48
|
+
null_byte: /\x00/,
|
49
|
+
excessive_parentheses: /\({5,}|\){5,}/
|
37
50
|
}.freeze
|
38
51
|
|
39
52
|
# SQL injection attack patterns - these are definitive threats
|
@@ -46,7 +59,19 @@ module Dbviewer
|
|
46
59
|
version_function: /version\(\)/i,
|
47
60
|
file_access: /\bLOAD_FILE\s*\(/i,
|
48
61
|
outfile_access: /\bINTO\s+OUTFILE\b/i,
|
49
|
-
dumpfile_access: /\bINTO\s+DUMPFILE\b/i
|
62
|
+
dumpfile_access: /\bINTO\s+DUMPFILE\b/i,
|
63
|
+
# Additional injection patterns for enhanced security
|
64
|
+
sleep_injection: /\b(SLEEP|WAITFOR|DELAY)\s*\(/i,
|
65
|
+
benchmark_injection: /\bBENCHMARK\s*\(/i,
|
66
|
+
information_schema: /\binformation_schema\./i,
|
67
|
+
mysql_user: /\bmysql\.user\b/i,
|
68
|
+
pg_user: /\bpg_user\b/i,
|
69
|
+
system_functions: /\b(system|exec|shell|cmd)\s*\(/i,
|
70
|
+
database_functions: /\b(database|schema|user|current_user)\s*\(\s*\)/i,
|
71
|
+
stacked_queries: /;\s*(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE)/i,
|
72
|
+
time_based_blind: /\bIF\s*\(\s*\d+\s*=\s*\d+\s*,\s*SLEEP\s*\(/i,
|
73
|
+
error_based: /\bCONVERT\s*\(\s*INT\s*,/i,
|
74
|
+
xpath_injection: /\bEXTRACTVALUE\s*\(/i
|
50
75
|
}.freeze
|
51
76
|
|
52
77
|
# Database feature detection patterns for query analysis
|
@@ -46,24 +46,28 @@ module Dbviewer
|
|
46
46
|
basic_validation_result = perform_basic_validation(sql)
|
47
47
|
return basic_validation_result if basic_validation_result
|
48
48
|
|
49
|
-
# Step 2:
|
49
|
+
# Step 2: Normalize the query early for forbidden keyword checks
|
50
|
+
normalized_sql = QueryNormalizer.normalize(sql)
|
51
|
+
|
52
|
+
# Step 3: Check for forbidden keywords in multiple statements first
|
53
|
+
forbidden_keyword_result = validate_forbidden_keywords_in_statements(normalized_sql)
|
54
|
+
return forbidden_keyword_result if forbidden_keyword_result
|
55
|
+
|
56
|
+
# Step 4: Security threat detection (on original SQL before normalization)
|
50
57
|
threat_validation_result = perform_threat_validation(sql)
|
51
58
|
return threat_validation_result if threat_validation_result
|
52
59
|
|
53
|
-
# Step
|
54
|
-
normalized_sql = QueryNormalizer.normalize(sql)
|
55
|
-
|
56
|
-
# Step 4: Handle special cases (PRAGMA) - only if allowed
|
60
|
+
# Step 5: Handle special cases (PRAGMA) - only if allowed
|
57
61
|
if allow_pragma
|
58
62
|
pragma_result = handle_pragma_statements(normalized_sql)
|
59
63
|
return pragma_result if pragma_result
|
60
64
|
end
|
61
65
|
|
62
|
-
# Step
|
66
|
+
# Step 6: Validate query structure and keywords
|
63
67
|
structure_validation_result = perform_structure_validation(normalized_sql)
|
64
68
|
return structure_validation_result if structure_validation_result
|
65
69
|
|
66
|
-
# Step
|
70
|
+
# Step 7: Check for multiple statements
|
67
71
|
multiple_statements_result = validate_single_statement(normalized_sql)
|
68
72
|
return multiple_statements_result if multiple_statements_result
|
69
73
|
|
@@ -97,6 +101,7 @@ module Dbviewer
|
|
97
101
|
# Perform security threat detection
|
98
102
|
def perform_threat_validation(sql)
|
99
103
|
if ThreatDetector.has_suspicious_patterns?(sql)
|
104
|
+
log_security_threat("suspicious_patterns", sql)
|
100
105
|
return ValidationResult.new(
|
101
106
|
valid?: false,
|
102
107
|
error_message: "Query contains suspicious patterns that may indicate SQL injection"
|
@@ -104,6 +109,7 @@ module Dbviewer
|
|
104
109
|
end
|
105
110
|
|
106
111
|
if ThreatDetector.has_injection_patterns?(sql)
|
112
|
+
log_security_threat("injection_patterns", sql)
|
107
113
|
return ValidationResult.new(
|
108
114
|
valid?: false,
|
109
115
|
error_message: "Query contains patterns commonly associated with SQL injection attempts"
|
@@ -145,6 +151,32 @@ module Dbviewer
|
|
145
151
|
nil # Structure validation passed
|
146
152
|
end
|
147
153
|
|
154
|
+
# Validate forbidden keywords specifically in multiple statements
|
155
|
+
def validate_forbidden_keywords_in_statements(normalized_sql)
|
156
|
+
statements = normalized_sql.split(";").reject { |s| s.nil? || s.strip.empty? }
|
157
|
+
|
158
|
+
if statements.size > 1
|
159
|
+
# Check each statement for forbidden keywords
|
160
|
+
statements.each do |statement|
|
161
|
+
forbidden_keyword = detect_forbidden_keywords(statement.strip)
|
162
|
+
if forbidden_keyword
|
163
|
+
return ValidationResult.new(
|
164
|
+
valid?: false,
|
165
|
+
error_message: "Forbidden keyword '#{forbidden_keyword}' detected in query"
|
166
|
+
)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# If multiple statements but no forbidden keywords, still reject for multiple statements
|
171
|
+
return ValidationResult.new(
|
172
|
+
valid?: false,
|
173
|
+
error_message: "Multiple SQL statements are not allowed"
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
nil # Single statement, proceed with normal validation
|
178
|
+
end
|
179
|
+
|
148
180
|
# Validate that query contains only a single statement
|
149
181
|
def validate_single_statement(normalized_sql)
|
150
182
|
statements = normalized_sql.split(";").reject { |s| s.nil? || s.strip.empty? }
|
@@ -204,6 +236,26 @@ module Dbviewer
|
|
204
236
|
def respond_to_missing?(method_name, include_private = false)
|
205
237
|
[ :has_suspicious_patterns?, :has_injection_patterns?, :normalize ].include?(method_name) || super
|
206
238
|
end
|
239
|
+
|
240
|
+
# Log security threats for monitoring and analysis
|
241
|
+
def log_security_threat(threat_type, sql)
|
242
|
+
if defined?(Rails) && Rails.logger
|
243
|
+
Rails.logger.warn("[DBViewer][Security] SQL threat detected - #{threat_type}: #{sql.truncate(200)}")
|
244
|
+
end
|
245
|
+
|
246
|
+
# Also log to query logger if available
|
247
|
+
if defined?(::Dbviewer::Query::Logger)
|
248
|
+
::Dbviewer::Query::Logger.log_security_event(
|
249
|
+
event_type: "threat_detected",
|
250
|
+
query_type: threat_type,
|
251
|
+
sql: sql,
|
252
|
+
timestamp: Time.current
|
253
|
+
)
|
254
|
+
end
|
255
|
+
rescue => e
|
256
|
+
# Don't let logging errors break the validation
|
257
|
+
puts "[DBViewer] Security logging error: #{e.message}"
|
258
|
+
end
|
207
259
|
end
|
208
260
|
end
|
209
261
|
end
|
data/lib/dbviewer/version.rb
CHANGED