PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
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 2.6.4 2.6.3 All 139 releases
metasync / redirections / class-metasync-redirection-validator.php

class-metasync-redirection-validator.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.0, at redirections/class-metasync-redirection-validator.php

168 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Shared validation helpers for redirection destinations and regex patterns.
5 *
6 * Centralises the checks that every write path (admin form, importers, MCP
7 * tools, engine render) must agree on, so a fix in one place cannot drift
8 * away from the others.
9 *
10 * @link https://searchatlas.com
11 * @since 1.0.0
12 * @package Metasync
13 * @subpackage Metasync/redirections
14 * @author Engineering Team <support@searchatlas.com>
15 */
16
17 # Abort if this file is accessed directly.
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 class Metasync_Redirection_Validator
23 {
24 /**
25 * Maximum accepted regex pattern length.
26 */
27 const MAX_REGEX_LENGTH = 500;
28
29 /**
30 * Reject destination syntax that slips past wp_validate_redirect.
31 *
32 * Browsers treat a backslash as a path separator, so '/\evil.com' passes
33 * wp_validate_redirect (parse_url sees no host) yet navigates off-site.
34 * A protocol-relative '//evil.com' host is likewise never an internal
35 * destination no matter how it was stored.
36 *
37 * @param string $url Destination as stored/entered.
38 * @return bool True when the syntax itself is not an evasion vector.
39 */
40 public static function is_safe_destination_syntax($url)
41 {
42 $url = (string) $url;
43
44 if (strpos($url, '\\') !== false) {
45 return false;
46 }
47
48 if (strpos($url, '//') === 0) {
49 return false;
50 }
51
52 return true;
53 }
54
55 /**
56 * Normalize a destination so wp_validate_redirect sees its true shape.
57 *
58 * Legacy/imported rows may contain backslashes; converting them to
59 * forward slashes lets the host check actually run.
60 *
61 * @param string $url Destination to normalize.
62 * @return string Normalized URL.
63 */
64 public static function normalize_destination($url)
65 {
66 return str_replace('\\', '/', (string) $url);
67 }
68
69 /**
70 * Screen a regex pattern for catastrophic backtracking before it is stored.
71 *
72 * Applies a hard length cap and rejects nested quantification — a
73 * quantified group whose body itself contains a quantifier, including
74 * via alternation ('(a+)*', '((a|a)*)*') — plus the legacy quantified
75 * character-class shapes.
76 *
77 * @param mixed $pattern Pattern without delimiters. Accepts any type: the
78 * admin path derives it from preg_replace(), which
79 * returns null on failure, and importers pass raw
80 * column values straight from a foreign table.
81 * @return bool True when the pattern is safe enough to store.
82 */
83 public static function is_regex_safe($pattern)
84 {
85 if (!is_string($pattern) || $pattern === '') {
86 return true;
87 }
88
89 if (strlen($pattern) > self::MAX_REGEX_LENGTH) {
90 return false;
91 }
92
93 if (self::has_nested_quantified_group($pattern)) {
94 return false;
95 }
96
97 // Legacy guard: quantifier inside a group with the group quantified,
98 // or a doubled quantifier on a character class.
99 if (preg_match('/(\([^)]*[+*][^)]*\))[+*?{]|(\[[^\]]*\])[+*][+*?{]/', $pattern)) {
100 return false;
101 }
102
103 return true;
104 }
105
106 /**
107 * Detect a quantified group whose body contains another quantifier.
108 *
109 * '(a+)*' and '((a|a)*)*' blow up exponentially on backtracking; the
110 * simpler '(a|a)+' stays linear and is allowed. Escaped characters and
111 * character-class contents are ignored so '(?:[a+])*' is not a false hit.
112 *
113 * @param string $pattern Pattern without delimiters.
114 * @return bool True when nested quantification is present.
115 */
116 private static function has_nested_quantified_group($pattern)
117 {
118 $len = strlen($pattern);
119
120 for ($i = 0; $i < $len; $i++) {
121 if ($pattern[$i] !== ')' || $i + 1 >= $len) {
122 continue;
123 }
124
125 $next = $pattern[$i + 1];
126 $quantified = ($next === '*' || $next === '+' || $next === '?');
127 if (!$quantified && $next === '{') {
128 // Only a real {n[,m]} bound counts, not '{' as a literal.
129 $quantified = (bool) preg_match('/^\{\d+(,\d*)?\}/', substr($pattern, $i + 1));
130 }
131 if (!$quantified) {
132 continue;
133 }
134
135 // Walk back to the '(' matching this ')'.
136 $depth = 1;
137 $j = $i - 1;
138 while ($j >= 0 && $depth > 0) {
139 $char = $pattern[$j];
140 if ($char === ')') {
141 $depth++;
142 } elseif ($char === '(') {
143 $depth--;
144 } elseif ($char === '\\') {
145 $j--; // Skip the escaped character.
146 }
147 $j--;
148 }
149 if ($depth !== 0) {
150 continue; // Unbalanced; validity is checked elsewhere.
151 }
152
153 $body_start = $j + 2;
154 $body = substr($pattern, $body_start, $i - $body_start);
155
156 // Ignore escaped characters and character-class contents.
157 $body = preg_replace('/\\\\./s', '', $body);
158 $body = preg_replace('/\[[^\]]*\]/', '', $body);
159
160 if ($body !== null && preg_match('/[+*]|\{\d+,?\d*\}/', $body)) {
161 return true;
162 }
163 }
164
165 return false;
166 }
167 }
168