From cff0d3638eb7aaf1f16628809eede643cea034a2 Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 20:54:03 +0200 Subject: [PATCH 1/4] Improve SimplePie redirects (POST to GET, remove authentication cross-origin) --- src/File.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/File.php b/src/File.php index 13a50afdf..87869dbd7 100644 --- a/src/File.php +++ b/src/File.php @@ -179,6 +179,45 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, $this->success = false; return; } + + // FreshRSS: POST to GET on redirect + if (isset($curl_options[CURLOPT_POST]) && in_array($this->status_code, [301, 302, 303], true)) { // Not for 307 and 308, which must not change the HTTP method + unset($curl_options[CURLOPT_POST]); + unset($curl_options[CURLOPT_POSTFIELDS]); + if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { + $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => + is_string($header) && !str_starts_with(strtolower(trim($header)), 'content-type:')); + } + } + // FreshRSS: cross-origin authentication headers removal + if (($url_parts_from = parse_url($url)) === false) { + throw new \InvalidArgumentException('Malformed URL: ' . $url); + } + if (($url_parts_to = parse_url($location)) === false) { + $this->error = "Invalid redirect location: malformed URL “{$url}”"; + $this->success = false; + return; + } + foreach ([&$url_parts_from, &$url_parts_to] as &$url_parts) { + $url_parts['port'] ??= match ($url_parts['scheme']) { + 'http' => 80, + 'https' => 443, + default => 0, + }; + } + unset($url_parts); + $sameOriginRedirect = + ($url_parts_from['scheme'] ?? '') === ($url_parts_to['scheme'] ?? '') && + ($url_parts_from['host'] ?? '') === ($url_parts_to['host'] ?? '') && + ($url_parts_from['port'] ?? '') === ($url_parts_to['port'] ?? ''); + if (!$sameOriginRedirect) { + unset($curl_options[CURLOPT_COOKIE]); + if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { + $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => + is_string($header) && !preg_match('/^(Cookie|Authorization)\\s*:/i', $header)); + } + } + $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); return; From 0c3b3d7a55ed3c8fd86f2b0ad5d80f41634cf86a Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 21:18:55 +0200 Subject: [PATCH 2/4] PHP 7.2+ compatibility --- src/File.php | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/File.php b/src/File.php index 87869dbd7..14cf37fcf 100644 --- a/src/File.php +++ b/src/File.php @@ -185,8 +185,12 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, unset($curl_options[CURLOPT_POST]); unset($curl_options[CURLOPT_POSTFIELDS]); if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { - $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => - is_string($header) && !str_starts_with(strtolower(trim($header)), 'content-type:')); + $curl_options[CURLOPT_HTTPHEADER] = array_filter( + $curl_options[CURLOPT_HTTPHEADER], + function ($header) { + return is_string($header) && substr(strtolower(trim($header)), 0, 13) !== 'content-type:'; + } + ); } } // FreshRSS: cross-origin authentication headers removal @@ -199,11 +203,13 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, return; } foreach ([&$url_parts_from, &$url_parts_to] as &$url_parts) { - $url_parts['port'] ??= match ($url_parts['scheme']) { - 'http' => 80, - 'https' => 443, - default => 0, - }; + if (!isset($url_parts['port']) && isset($url_parts['scheme'])) { + if ($url_parts['scheme'] === 'http') { + $url_parts['port'] = 80; + } elseif ($url_parts['scheme'] === 'https') { + $url_parts['port'] = 443; + } + } } unset($url_parts); $sameOriginRedirect = @@ -213,8 +219,12 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, if (!$sameOriginRedirect) { unset($curl_options[CURLOPT_COOKIE]); if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { - $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => - is_string($header) && !preg_match('/^(Cookie|Authorization)\\s*:/i', $header)); + $curl_options[CURLOPT_HTTPHEADER] = array_filter( + $curl_options[CURLOPT_HTTPHEADER], + function ($header) { + return is_string($header) && !preg_match('/^(Cookie|Authorization)\s*:/i', $header); + } + ); } } From 0d69814c6b880fbeabb5b7ded2fee29dee17f899 Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 23:20:23 +0200 Subject: [PATCH 3/4] Normalize URLs to lowercase when parsing --- src/File.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/File.php b/src/File.php index 14cf37fcf..576ac5b23 100644 --- a/src/File.php +++ b/src/File.php @@ -194,10 +194,10 @@ function ($header) { } } // FreshRSS: cross-origin authentication headers removal - if (($url_parts_from = parse_url($url)) === false) { + if (($url_parts_from = parse_url(strtolower($url))) === false) { throw new \InvalidArgumentException('Malformed URL: ' . $url); } - if (($url_parts_to = parse_url($location)) === false) { + if (($url_parts_to = parse_url(strtolower($location))) === false) { $this->error = "Invalid redirect location: malformed URL “{$url}”"; $this->success = false; return; From 4c8a01258991bd7ddc2d709d92dd778ab4eaafa1 Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 23:32:19 +0200 Subject: [PATCH 4/4] Fix wrong URL printed in error message --- src/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 576ac5b23..d0e526c3d 100644 --- a/src/File.php +++ b/src/File.php @@ -198,7 +198,7 @@ function ($header) { throw new \InvalidArgumentException('Malformed URL: ' . $url); } if (($url_parts_to = parse_url(strtolower($location))) === false) { - $this->error = "Invalid redirect location: malformed URL “{$url}”"; + $this->error = "Invalid redirect location: malformed URL “{$location}”"; $this->success = false; return; }