1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
|
<?php
namespace Automattic\WooCommerce\Blueprint\Importers;
use Automattic\WooCommerce\Blueprint\StepProcessor; use Automattic\WooCommerce\Blueprint\StepProcessorResult; use Automattic\WooCommerce\Blueprint\Steps\RunSql; use Automattic\WooCommerce\Blueprint\UsePluginHelpers; use Automattic\WooCommerce\Blueprint\UseWPFunctions;
/** * Processes SQL execution steps in the Blueprint. * * Handles the execution of SQL queries with safety checks to prevent * unauthorized modifications to sensitive WordPress data. * * @package Automattic\WooCommerce\Blueprint\Importers */ class ImportRunSql implements StepProcessor { use UsePluginHelpers; use UseWPFunctions;
/** * List of allowed SQL query types. * * @var array */ private const ALLOWED_QUERY_TYPES = array( 'INSERT', 'UPDATE', 'REPLACE INTO', );
/** * Process the SQL execution step. * * Validates and executes the SQL query while ensuring: * 1. Only allowed query types are executed * 2. No modifications to admin users or roles * 3. No unauthorized changes to user capabilities * * @param object $schema The schema containing the SQL query to execute. * @return StepProcessorResult The result of the SQL execution. */ public function process( $schema ): StepProcessorResult { global $wpdb; $result = StepProcessorResult::success( RunSql::get_step_name() );
$sql = trim( $schema->sql->contents );
// Check if the query type is allowed. if ( ! $this->is_allowed_query_type( $sql ) ) { $result->add_error( sprintf( 'Only %s queries are allowed.', implode( ', ', self::ALLOWED_QUERY_TYPES ) ) ); return $result; }
// Check for SQL comments that might be hiding malicious code. if ( $this->contains_suspicious_comments( $sql ) ) { $result->add_error( 'SQL query contains suspicious comment patterns.' ); return $result; }
// Detect SQL injection patterns. if ( $this->contains_sql_injection_patterns( $sql ) ) { $result->add_error( 'SQL query contains potential injection patterns.' ); return $result; }
// Check if the query affects protected tables. if ( $this->affects_protected_tables( $sql ) ) { $result->add_error( 'Modifications to admin users or roles are not allowed.' ); return $result; }
// Check if the query affects user capabilities in wp_options. if ( $this->affects_user_capabilities( $sql ) ) { $result->add_error( 'Modifications to user roles or capabilities are not allowed.' ); return $result; }
$wpdb->suppress_errors( true ); $wpdb->query( 'START TRANSACTION' );
try { $query_result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$last_error = $wpdb->last_error; if ( $last_error ) { $wpdb->query( 'ROLLBACK' ); $result->add_error( 'Error executing SQL: ' . $last_error ); } else { $wpdb->query( 'COMMIT' ); $result->add_debug( "Executed SQL ({$schema->sql->name}): Affected {$query_result} rows" ); } } catch ( \Throwable $e ) { $wpdb->query( 'ROLLBACK' ); $result->add_error( "Exception executing SQL: {$e->getMessage()}" ); }
return $result; }
/** * Returns the class name of the step this processor handles. * * @return string The class name of the step this processor handles. */ public function get_step_class(): string { return RunSql::class; }
/** * Check if the current user has the required capabilities for this step. * * @param object $schema The schema to process. * * @return bool True if the user has the required capabilities. False otherwise. */ public function check_step_capabilities( $schema ): bool { if ( ! current_user_can( 'manage_options' ) ) { return false; }
if ( ! current_user_can( 'edit_posts' ) ) { return false; }
if ( ! current_user_can( 'edit_users' ) ) { return false; }
return true; } /** * Check if the SQL query type is allowed. * * @param string $sql_content The SQL query to check. * @return bool True if the query type is allowed, false otherwise. */ private function is_allowed_query_type( string $sql_content ): bool { $uppercase_sql_content = strtoupper( trim( $sql_content ) );
foreach ( self::ALLOWED_QUERY_TYPES as $query_type ) { if ( 0 === stripos( $uppercase_sql_content, $query_type ) ) { return true; } } return false; }
/** * Check for suspicious comment patterns that might hide malicious code. * * This method detects various types of SQL comments that might be used * to hide malicious SQL commands or bypass security filters. * * @param string $sql_content The SQL query to check. * @return bool True if suspicious comments found, false otherwise. */ private function contains_suspicious_comments( string $sql_content ): bool { // Quick check if there are any comments at all before running regex. if ( strpos( $sql_content, '--' ) === false && strpos( $sql_content, '/*' ) === false && strpos( $sql_content, '#' ) === false ) { return false; }
// List of potentially dangerous SQL commands to check for in comments. $dangerous_commands = array( 'DELETE', 'DROP', 'ALTER', 'CREATE', 'TRUNCATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'CALL', 'INTO OUTFILE', 'INTO DUMPFILE', 'LOAD_FILE', 'LOAD DATA', 'BENCHMARK', 'SLEEP', 'INFORMATION_SCHEMA', 'USER\\(', 'DATABASE\\(', 'SCHEMA\\(', );
$dangerous_pattern = implode( '|', $dangerous_commands );
// Check for SQL comments that might be hiding malicious code. $patterns = array( // Single-line comments (-- style) containing dangerous commands. '/--.*?(' . $dangerous_pattern . ')/i', // Single-line comments (# style) containing dangerous commands. '/#.*?(' . $dangerous_pattern . ')/i', // Multi-line comments hiding dangerous commands. '/\/\*.*?(' . $dangerous_pattern . ').*?\*\//is', // MySQL-specific execution comments (version-specific code execution). '/\/\*![0-9]*.*?\*\//', );
foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $sql_content ) ) { return true; } } return false; }
/** * Check for common SQL injection patterns. * * @param string $sql_content The SQL query to check. * @return bool True if potential injection patterns found, false otherwise. */ private function contains_sql_injection_patterns( string $sql_content ): bool { $patterns = array( '/UNION\s+(?:ALL\s+)?SELECT/i', // UNION-based injections. '/OR\s+1\s*=\s*1/i', // OR 1=1 condition. '/AND\s+0\s*=\s*0/i', // AND 0=0 condition. '/;\s*--/i', // Inline comment terminations. '/SLEEP\s*\(/i', // Time-based injections. '/BENCHMARK\s*\(/i', // Benchmark-based injections. '/LOAD_FILE\s*\(/i', // File access. '/INTO\s+OUTFILE/i', // File write. '/INTO\s+DUMPFILE/i', // File dump. '/CREATE\s+(?:TEMPORARY\s+)?TABLE/i', // Table creation. '/DROP\s+TABLE/i', // Table deletion. '/ALTER\s+TABLE/i', // Table alteration. '/INFORMATION_SCHEMA/i', // Database metadata access. '/EXEC\s*\(/i', // Stored procedure execution. '/SCHEMA_NAME/i', // Schema access. '/DATABASE\(\)/i', // Current database name. '/CHR\s*\(/i', // Character function for evasion. '/CHAR\s*\(/i', // Character function for evasion. '/FROM\s+mysql\./i', // Direct MySQL system table access. '/FROM\s+information_schema\./i', // Direct information schema access. ); foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $sql_content ) ) { return true; } }
return false; }
/** * Check if the SQL query affects protected user tables. * * @param string $sql_content The SQL query to check. * @return bool True if the query affects protected tables, false otherwise. */ private function affects_protected_tables( string $sql_content ): bool { global $wpdb; $protected_tables = array( $wpdb->users, $wpdb->usermeta, );
foreach ( $protected_tables as $table ) { if ( preg_match( '/\b' . preg_quote( $table, '/' ) . '\b/i', $sql_content ) ) { return true; } }
return false; }
/** * Check if the SQL query affects user capabilities in wp_options. * * @param string $sql_content The SQL query to check. * @return bool True if the query affects user capabilities, false otherwise. */ private function affects_user_capabilities( string $sql_content ): bool { global $wpdb;
// Check if the query affects user capabilities in wp_options. if ( stripos( $sql_content, $wpdb->prefix . 'options' ) !== false ) { $option_patterns = array( 'user_roles', 'capabilities', 'wp_user_', 'role_', 'administrator', );
foreach ( $option_patterns as $pattern ) { if ( stripos( $sql_content, $pattern ) !== false ) { return true; } } } return false; } }
|