diff --git a/includes/class-redirect.php b/includes/class-redirect.php index 299eec1..6ba7f3d 100644 --- a/includes/class-redirect.php +++ b/includes/class-redirect.php @@ -58,7 +58,7 @@ final class Redirect if (!$orig_id) { return; } - $orig_permalink = \get_permalink($orig_id); + $orig_permalink = Helpers::without_url_filters(static fn () => \get_permalink($orig_id)); if (!is_string($orig_permalink) || $orig_permalink === '') { return; } diff --git a/includes/class-rest-url-filter.php b/includes/class-rest-url-filter.php index 716ae4e..5a8f8ac 100644 --- a/includes/class-rest-url-filter.php +++ b/includes/class-rest-url-filter.php @@ -16,7 +16,13 @@ final class RestUrlFilter if ($host === '') { return $url; } - $replaced = preg_replace('#^https?://[^/]+#', 'https://' . $host, $url); - return is_string($replaced) ? $replaced : $url; + $parsed = parse_url($url); + if ($parsed === false || empty($parsed['scheme'])) { + return $url; + } + $path = $parsed['path'] ?? ''; + $query = isset($parsed['query']) ? '?' . $parsed['query'] : ''; + $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; + return 'https://' . $host . $path . $query . $fragment; } } diff --git a/tests/RestUrlFilterTest.php b/tests/RestUrlFilterTest.php index 7fff6f4..9829f3e 100644 --- a/tests/RestUrlFilterTest.php +++ b/tests/RestUrlFilterTest.php @@ -34,4 +34,16 @@ final class RestUrlFilterTest extends TestCase $url = 'https://other.example/wp-json/wp/v2/posts'; self::assertSame($url, RestUrlFilter::preserve_host($url)); } + + public function test_preserve_host_does_not_interpret_replacement_metachars_in_host(): void + { + // $_SERVER['HTTP_HOST'] is attacker-controlled (Host header). + // The replacement string must not interpret $0/\\1 as backreferences. + $_SERVER['HTTP_HOST'] = 'evil$0.com'; + + self::assertSame( + 'https://evil$0.com/wp-json/wp/v2/posts', + RestUrlFilter::preserve_host('http://example.org/wp-json/wp/v2/posts') + ); + } }