PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | includes/class-metasync-redirections-admin.php +29 -18 2.6.21 → 2.7.1 View file →
@@ -65,9 +65,9 @@
65 65 $this->check_database_structure();
66 66
67 67 $this->ensure_404_monitor_table();
68 68
69 - $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
69 + $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
70 70
71 71 $this->add_tabbed_interface_assets();
72 72 $this->admin->render_layout_open('Redirections', 'redirections', 'Manage URL redirects and monitor 404 errors on your site.');
73 73 $this->render_tab_navigation($current_tab);
@@ -101,9 +101,9 @@
101 101
102 102 private function safe_redirect($url)
103 103 {
104 104 if (!headers_sent()) {
105 - wp_redirect($url);
105 + wp_safe_redirect($url);
106 106 exit;
107 107 } else {
108 108 echo '<script type="text/javascript">window.location.href = "' . esc_url($url) . '";</script>';
109 109 echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_url($url) . '"></noscript>';
@@ -135,15 +135,18 @@
135 135 if (!Metasync::current_user_has_plugin_access()) {
136 136 wp_die('Insufficient permissions.');
137 137 }
138 138
139 - $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', $_POST['source_url']) : [];
140 - $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', $_POST['search_type']) : [];
141 - $destination_url = isset($_POST['destination_url']) ? sanitize_text_field($_POST['destination_url']) : '';
139 + // Unslash before sanitizing: source URLs with apostrophes/backslashes
140 + // must be stored in their literal form so the front-end matcher (which
141 + // compares against the unslashed request URI) can hit them.
142 + $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', wp_unslash($_POST['source_url'])) : [];
143 + $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', wp_unslash($_POST['search_type'])) : [];
144 + $destination_url = isset($_POST['destination_url']) ? sanitize_text_field(wp_unslash($_POST['destination_url'])) : '';
142 145 $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301;
143 - $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active';
146 + $status = isset($_POST['status']) ? sanitize_text_field(wp_unslash($_POST['status'])) : 'active';
144 147 $regex_pattern = isset($_POST['regex_pattern']) ? wp_unslash(trim($_POST['regex_pattern'])) : '';
145 - $description = isset($_POST['description']) ? sanitize_text_field($_POST['description']) : '';
148 + $description = isset($_POST['description']) ? sanitize_text_field(wp_unslash($_POST['description'])) : '';
146 149 $redirect_id = isset($_POST['redirect_id']) ? intval($_POST['redirect_id']) : 0;
147 150
148 151 $validation_errors = [];
149 152
@@ -187,8 +190,14 @@
187 190 if (empty($trimmed_dest)) {
188 191 $validation_errors[] = 'Destination URL is required for this redirect type.';
189 192 } elseif (!$this->is_valid_url($trimmed_dest)) {
190 193 $validation_errors[] = 'Invalid destination URL format. URLs should start with / for relative paths or be complete URLs.';
194 + } elseif (!Metasync_Redirection_Validator::is_safe_destination_syntax($trimmed_dest)) {
195 + // Browsers treat backslashes as path separators, so '/\evil.com'
196 + // passes wp_validate_redirect yet navigates off-site; a leading
197 + // '//' is a protocol-relative host. Neither is a valid internal
198 + // destination regardless of the external-redirects setting.
199 + $validation_errors[] = 'Destination URL contains invalid characters.';
191 200 } elseif (!get_option('metasync_allow_external_redirects', 0) && wp_validate_redirect($trimmed_dest, '') !== $trimmed_dest) {
192 201 $validation_errors[] = 'Destination URL must point to this site. External redirect destinations are not allowed. Enable "Allow External Redirects" in the Redirections settings to permit off-site redirects.';
193 202 }
194 203 }
@@ -234,12 +243,13 @@
234 243 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
235 244 return;
236 245 }
237 246
238 - // Reject patterns with nested quantifiers that could cause ReDoS
247 + // Reject patterns with nested quantification that could cause ReDoS,
248 + // including alternation blowup ('((a|a)*)*') the older guard missed.
239 249 $raw_check = preg_replace('/^\S(.*)\S[a-zA-Z]*$/', '$1', $regex_pattern);
240 - if (preg_match('/(\([^)]*[+*][^)]*\))[+*?{]|(\[[^\]]*\])[+*][+*?{]/', $raw_check)) {
241 - set_transient('metasync_redirection_error_' . $uid, 'Regex pattern contains potentially unsafe nested quantifiers.', 45);
250 + if (!Metasync_Redirection_Validator::is_regex_safe($raw_check)) {
251 + set_transient('metasync_redirection_error_' . $uid, 'Regex pattern is too long or contains potentially unsafe nested quantifiers (including nested alternation).', 45);
242 252 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
243 253 return;
244 254 }
245 255
@@ -272,8 +282,9 @@
272 282
273 283 // Loop detection: refuse to persist a chain that would resolve back to any source
274 284 if (!in_array($redirect_type, [410, 451])) {
275 285 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php';
286 + require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-validator.php';
276 287 $redirection_helper = new Metasync_Redirection($this->db_redirection);
277 288 foreach (array_keys($sources_from) as $source_url) {
278 289 $loop_error = $redirection_helper->validate_no_loop($source_url, $destination_url);
279 290 if ($loop_error !== null) {
@@ -315,9 +326,9 @@
315 326 $message = 'Redirection added successfully.';
316 327 }
317 328
318 329 } catch (Exception $e) {
319 - error_log('MetaSync error: ' . $e->getMessage());
330 + error_log('MetaSync error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
320 331 set_transient('metasync_redirection_error_' . $uid, 'An error occurred while saving the redirection. Please try again.', 45);
321 332 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
322 333 }
323 334
@@ -540,9 +551,9 @@
540 551
541 552 private function render_redirections_tab($current_tab = null)
542 553 {
543 554 if ($current_tab === null) {
544 - $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
555 + $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
545 556 }
546 557 $active_class = $current_tab === 'redirections' ? 'active' : '';
547 558
548 559 ?>
@@ -557,9 +568,9 @@
557 568
558 569 private function render_404_monitor_tab($current_tab = null)
559 570 {
560 571 if ($current_tab === null) {
561 - $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
572 + $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
562 573 }
563 574 $active_class = $current_tab === '404-monitor' ? 'active' : '';
564 575
565 576 ?>
@@ -574,12 +585,12 @@
574 585
575 586 $ErrorMonitor->create_admin_plugin_interface();
576 587
577 588 } catch (Exception $e) {
578 - error_log('MetaSync 404 Monitor Error: ' . $e->getMessage());
589 + error_log('MetaSync 404 Monitor Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
579 590 echo '<div class="notice notice-error"><p>An error occurred while loading the 404 monitor. Please check the server logs for details.</p></div>';
580 591 } catch (Error $e) {
581 - error_log('MetaSync 404 Monitor Fatal Error: ' . $e->getMessage());
592 + error_log('MetaSync 404 Monitor Fatal Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
582 593 echo '<div class="notice notice-error"><p>A fatal error occurred while loading the 404 monitor. Please check the server logs for details.</p></div>';
583 594 }
584 595 ?>
585 596 </div>
@@ -592,9 +603,9 @@
592 603 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
593 604
594 605 $table_name = $wpdb->prefix . Metasync_Error_Monitor_Database::$table_name;
595 606
596 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
607 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin repair action — re-runs the migration check on demand from the UI
597 608 require_once plugin_dir_path(dirname(__FILE__)) . 'database/class-db-migrations.php';
598 609 MetaSync_DBMigration::run_migrations();
599 610 }
600 611 }
@@ -603,13 +614,13 @@
603 614 {
604 615 global $wpdb;
605 616 $table_name = $wpdb->prefix . 'metasync_redirections';
606 617
607 - if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
618 + if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin repair action — re-runs the migration check on demand from the UI
608 619 return;
609 620 }
610 621
611 - $columns = $wpdb->get_col("DESCRIBE {$table_name}");
622 + $columns = $wpdb->get_col("DESCRIBE {$wpdb->prefix}metasync_redirections"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin repair action — re-runs the migration check on demand from the UI
612 623 $required_columns = ['pattern_type', 'regex_pattern', 'description', 'created_at', 'updated_at', 'last_accessed_at'];
613 624
614 625 $missing_columns = array_diff($required_columns, $columns);
615 626