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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 897715943af7ce5d339aebb00b03bc13e1abc728c5e09d9615ec7189555cf014
4
- data.tar.gz: 3ee632c6e1a6cc9fe5e5df08277520a0ea6a705e20a84bcab1756acd27a0f031
3
+ metadata.gz: 73a576f4a8f66beb4c9320015567379b28baf8f11d857b4e5b69ff7372ec0977
4
+ data.tar.gz: f408948d8b866d8a223035555d31b9b5131f7d3c21ae4d2fe3864dfbd5670d75
5
5
  SHA512:
6
- metadata.gz: 010a246eb09d2a6841c01bb3b87553b89ed4050a8781572a5b016894b208688d495744a1539636e8655c6ea7778449a2d0b9c0e204b0be5d7b68c2ee6ac883e3
7
- data.tar.gz: e4142876a0867ebc55655469e8b85f240cc5a48b9b45ffb1d5bec89ab38939b95e996dcba1fa9b2555658cd1276415d24d14ae57ad98554b4f1a852beb8e0632
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** - Protect sensitive data with configurable masking rules
20
- - **Enhanced UI Features**
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. Query Log collector are disabled by default on non development environtment.
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 several security features to protect your database:
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
- - **HTTP Basic Authentication**: Protect access with username and password authentication
229
- - **Complete Disabling**: Completely disable DBViewer in production or sensitive environments
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
- You can enable HTTP Basic Authentication to secure access to DBViewer:
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, you can completely disable DBViewer:
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 where database admin tools should not be accessible.
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
- #### Quick Setup
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
- #### Built-in Masking Types
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 (current behavior)
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
- #### Whitelist Mode (Recommended for Production)
318
-
319
- Whitelist mode is the most secure approach, where only explicitly allowed tables can be accessed:
277
+ #### Configuration Examples
320
278
 
321
- ### Generate Example Configuration
322
-
323
- Use the generator to create an example PII configuration:
324
-
325
- ```bash
326
- rails generate dbviewer:install
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
- This creates `config/initializers/dbviewer_pii_example.rb` with comprehensive examples.
298
+ When access control is enabled, DBViewer will:
330
299
 
331
- For detailed PII masking documentation, see [PII_MASKING.md](docs/PII_MASKING.md).
332
-
333
- ## ๐Ÿ“ Security Note
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
- When used in production, ensure:
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.7.2"
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
- query = query.present? ? query.to_s : default_query(table_name)
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?(query)
12
- query = default_query(table_name)
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
- query
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(@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
- exec_query("PRAGMA #{pragma}")
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.any? do |_name, pattern|
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: Security threat detection (before normalization)
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 3: Normalize the query
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 5: Validate query structure and keywords
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 6: Check for multiple statements
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
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.9.1"
2
+ VERSION = "0.9.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh