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 +51 -18 2.6.14 → 2.7.1 View file →
@@ -15,8 +15,22 @@
15 15 class Metasync_Redirections_Admin
16 16 {
17 17 private static $instance = null;
18 18
19 + /**
20 + * Which tab of the combined Redirections screen is being viewed.
21 + *
22 + * Set ONLY by render_tab_content(), i.e. only when both the redirections
23 + * and 404-monitor tables are rendered onto one page. Stays null on the
24 + * standalone 404 Monitor page (page=...-404-monitor), where that table is
25 + * the only one present. The list tables read it to decide whether to offer
26 + * bulk actions - see get_bulk_actions() in either table for why.
27 + *
28 + * @var string|null 'redirections', '404-monitor', or null when not on the
29 + * combined screen.
30 + */
31 + public static $combined_screen_active_tab = null;
32 +
19 33 /** @var object Redirection database helper */
20 34 private $db_redirection;
21 35
22 36 /** @var Metasync_Admin Back-reference used for shared UI helpers */
@@ -51,9 +65,9 @@
51 65 $this->check_database_structure();
52 66
53 67 $this->ensure_404_monitor_table();
54 68
55 - $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
56 70
57 71 $this->add_tabbed_interface_assets();
58 72 $this->admin->render_layout_open('Redirections', 'redirections', 'Manage URL redirects and monitor 404 errors on your site.');
59 73 $this->render_tab_navigation($current_tab);
@@ -87,9 +101,9 @@
87 101
88 102 private function safe_redirect($url)
89 103 {
90 104 if (!headers_sent()) {
91 - wp_redirect($url);
105 + wp_safe_redirect($url);
92 106 exit;
93 107 } else {
94 108 echo '<script type="text/javascript">window.location.href = "' . esc_url($url) . '";</script>';
95 109 echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_url($url) . '"></noscript>';
@@ -121,15 +135,18 @@
121 135 if (!Metasync::current_user_has_plugin_access()) {
122 136 wp_die('Insufficient permissions.');
123 137 }
124 138
125 - $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', $_POST['source_url']) : [];
126 - $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', $_POST['search_type']) : [];
127 - $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'])) : '';
128 145 $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301;
129 - $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active';
146 + $status = isset($_POST['status']) ? sanitize_text_field(wp_unslash($_POST['status'])) : 'active';
130 147 $regex_pattern = isset($_POST['regex_pattern']) ? wp_unslash(trim($_POST['regex_pattern'])) : '';
131 - $description = isset($_POST['description']) ? sanitize_text_field($_POST['description']) : '';
148 + $description = isset($_POST['description']) ? sanitize_text_field(wp_unslash($_POST['description'])) : '';
132 149 $redirect_id = isset($_POST['redirect_id']) ? intval($_POST['redirect_id']) : 0;
133 150
134 151 $validation_errors = [];
135 152
@@ -173,8 +190,14 @@
173 190 if (empty($trimmed_dest)) {
174 191 $validation_errors[] = 'Destination URL is required for this redirect type.';
175 192 } elseif (!$this->is_valid_url($trimmed_dest)) {
176 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.';
177 200 } elseif (!get_option('metasync_allow_external_redirects', 0) && wp_validate_redirect($trimmed_dest, '') !== $trimmed_dest) {
178 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.';
179 202 }
180 203 }
@@ -220,12 +243,13 @@
220 243 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
221 244 return;
222 245 }
223 246
224 - // 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.
225 249 $raw_check = preg_replace('/^\S(.*)\S[a-zA-Z]*$/', '$1', $regex_pattern);
226 - if (preg_match('/(\([^)]*[+*][^)]*\))[+*?{]|(\[[^\]]*\])[+*][+*?{]/', $raw_check)) {
227 - 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);
228 252 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
229 253 return;
230 254 }
231 255
@@ -258,8 +282,9 @@
258 282
259 283 // Loop detection: refuse to persist a chain that would resolve back to any source
260 284 if (!in_array($redirect_type, [410, 451])) {
261 285 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php';
286 + require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-validator.php';
262 287 $redirection_helper = new Metasync_Redirection($this->db_redirection);
263 288 foreach (array_keys($sources_from) as $source_url) {
264 289 $loop_error = $redirection_helper->validate_no_loop($source_url, $destination_url);
265 290 if ($loop_error !== null) {
@@ -301,9 +326,9 @@
301 326 $message = 'Redirection added successfully.';
302 327 }
303 328
304 329 } catch (Exception $e) {
305 - 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
306 331 set_transient('metasync_redirection_error_' . $uid, 'An error occurred while saving the redirection. Please try again.', 45);
307 332 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
308 333 }
309 334
@@ -511,8 +536,16 @@
511 536 }
512 537
513 538 private function render_tab_content($current_tab)
514 539 {
540 + // Both tables are rendered onto this one page (CSS hides the inactive
541 + // tab), so record which tab is actually being viewed. The list tables
542 + // use this to keep the hidden tab from emitting a duplicate set of
543 + // bulk-action controls - see get_bulk_actions() in either table.
544 + self::$combined_screen_active_tab = ($current_tab === '404-monitor')
545 + ? '404-monitor'
546 + : 'redirections';
547 +
515 548 $this->render_redirections_tab($current_tab);
516 549 $this->render_404_monitor_tab($current_tab);
517 550 }
518 551
@@ -518,9 +551,9 @@
518 551
519 552 private function render_redirections_tab($current_tab = null)
520 553 {
521 554 if ($current_tab === null) {
522 - $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
523 556 }
524 557 $active_class = $current_tab === 'redirections' ? 'active' : '';
525 558
526 559 ?>
@@ -535,9 +568,9 @@
535 568
536 569 private function render_404_monitor_tab($current_tab = null)
537 570 {
538 571 if ($current_tab === null) {
539 - $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
540 573 }
541 574 $active_class = $current_tab === '404-monitor' ? 'active' : '';
542 575
543 576 ?>
@@ -552,12 +585,12 @@
552 585
553 586 $ErrorMonitor->create_admin_plugin_interface();
554 587
555 588 } catch (Exception $e) {
556 - 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
557 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>';
558 591 } catch (Error $e) {
559 - 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
560 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>';
561 594 }
562 595 ?>
563 596 </div>
@@ -570,9 +603,9 @@
570 603 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
571 604
572 605 $table_name = $wpdb->prefix . Metasync_Error_Monitor_Database::$table_name;
573 606
574 - 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
575 608 require_once plugin_dir_path(dirname(__FILE__)) . 'database/class-db-migrations.php';
576 609 MetaSync_DBMigration::run_migrations();
577 610 }
578 611 }
@@ -581,13 +614,13 @@
581 614 {
582 615 global $wpdb;
583 616 $table_name = $wpdb->prefix . 'metasync_redirections';
584 617
585 - 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
586 619 return;
587 620 }
588 621
589 - $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
590 623 $required_columns = ['pattern_type', 'regex_pattern', 'description', 'created_at', 'updated_at', 'last_accessed_at'];
591 624
592 625 $missing_columns = array_diff($required_columns, $columns);
593 626