mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-06-29 09:45:30 -04:00
* Add SSRF mitigations using `filter_var` and `CURLOPT_RESOLVE`
The idea is to prevent FreshRSS from sending any HTTP requests to internal services, except for the ones that are explicitly allowed in the config.
Based on 6e82b46a48/lib/filelib.php (L3818) and https://github.com/symfony/symfony/blob/8.1/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
https://github.com/FreshRSS/simplepie/pull/76
https://github.com/FreshRSS/simplepie/pull/78
* Add allowlist setting in Web UI
* make readme
* Update app/i18n/fr/admin.php
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* make readme again
* make readme
* Further work
Still WIP and needs testing etc.
* Readd previous if check for domain combination allowlist
* Turn POST to GET after redirect
* Improve
* Update config.default.php
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* make readme
* Skip SSRF check if `CURLOPT_PROXY` is set
* make readme
* Fix `!empty()` mistake
* Respect max redirects feed option when fetching with `httpGet()`
* Respect max redirects during SimplePie fetching + fix bypass
bypass fix: `CURLOPT_FOLLOWLOCATION` was moved below so that emulated redirects are enforced.
* Avoid FreshRSS and Minz code in SimplePie
https://github.com/FreshRSS/FreshRSS/pull/8400#discussion_r2935375980
* Corrected hook code
* phpdoc wrong return type
* Add CIDR support in allowlist
* Implement simple DNS caching
* Suppress `dns_get_record()` warnings
* A bit of proof-reading
* Minor typo
* Fix proxy logic
* Fix HTTP POST redirect logic
* Proofread checkCIDR
Add fixes for several situations
* Remove credentials from URL in logs
* Ensure `CURLOPT_FOLLOWLOCATION` is `false` by setting it at the end
* Fix codesniffer long line
* Fix potential bypass due to wrong return value
If there were no records returned by `dns_get_record()`, no overrides to `CURLOPT_RESOLVE` would get passed,
and a potential bypass could occur, when cURL would try to resolve the domain by itself.
* Put the URL at the end in logs
* Add documentation and environment variable support
* make readme
* Fix wrong behavior in case of IP
* Fix duplicate selector in CSS
* Minor type check change
* i18n fr, en
* Minor type check change
* Fix whitespace i18n fr
* make fix-all
* Fix `$ips_ok` not being returned after domain records were cached
* make readme
* PHPStan fix
* make readme
* Minor syntax in SimplePie
* Only return `null` if no allowed IPs were found
* Add wildcard *, help message
* Consistent docs with help message
* i18n: pl
* SimplePie compatibility PHP 7.2
* make fix-all
* Sync SimplePie
* https://github.com/FreshRSS/simplepie/pull/76
* 💥 Breaking change in the Changelog
* Document `INTERNAL_HOST_ALLOWLIST` in Docker docs
* Remove `Cookie` and `Authorization` headers in `httpGet()` during cross-origin redirect
* Minor whitespace
And same comment convention than below
* Remove authentication headers and change POST to GET on redirect in SimplePie
* Remove .local in Docker example
* Fill in default ports when comparing URL origins
* Remove .local from other places than the Docker example
* Rewrite WebSub subscribe to use `httpGet()`
* make fix-all
* Also unset `CURLOPT_USERPWD` during redirects
* phpcs fix
* Always unset `CURLOPT_FOLLOWLOCATION`
* Bump SimplePie
https://github.com/FreshRSS/simplepie/pull/78
* Update logic for CURLOPT_FOLLOWLOCATION
* Fix PHPStan
* Changelog fix security section
* Update most common RSS Bridge case
https://hub.docker.com/r/rssbridge/rss-bridge
* Replace misleading 127.0.0.1:8080 example for Docker
This does not make sense for a Docker container
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
81 lines
2.9 KiB
PHP
81 lines
2.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
final class FreshRSS_SimplePieFetch extends \SimplePie\File
|
|
{
|
|
public function __construct(string $url, int $timeout = 10, int $redirects = 5,
|
|
?array $headers = null, ?string $useragent = null, bool $force_fsockopen = false, array $curl_options = []) {
|
|
|
|
// Remove case-insensitively default HTTP headers passed by $headers if they are overridden by $curl_options[CURLOPT_HTTPHEADER]
|
|
if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null) && is_array($headers)) {
|
|
$existingKeys = [];
|
|
foreach ($curl_options[CURLOPT_HTTPHEADER] as $headerLine) {
|
|
if (!is_string($headerLine)) {
|
|
continue;
|
|
}
|
|
$parts = explode(':', $headerLine, 2);
|
|
if (count($parts) >= 2) {
|
|
$existingKeys[strtolower(trim($parts[0]))] = true;
|
|
}
|
|
}
|
|
foreach ($headers as $key => $value) {
|
|
if (isset($existingKeys[strtolower($key)])) {
|
|
unset($headers[$key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
$redirects = $curl_options[CURLOPT_MAXREDIRS] ?? null;
|
|
if (!is_int($redirects)) {
|
|
$redirects = 4;
|
|
} elseif ($redirects < 0) {
|
|
$redirects = -1; // infinite redirects
|
|
}
|
|
if (isset($curl_options[CURLOPT_FOLLOWLOCATION])) {
|
|
if ($curl_options[CURLOPT_FOLLOWLOCATION] == true) {
|
|
unset($curl_options[CURLOPT_FOLLOWLOCATION]); // Favour the custom SimplePie redirects for security
|
|
} else {
|
|
$curl_options[CURLOPT_FOLLOWLOCATION] = false;
|
|
unset($curl_options[CURLOPT_MAXREDIRS]);
|
|
$redirects = 0;
|
|
}
|
|
}
|
|
|
|
parent::__construct($url, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options);
|
|
}
|
|
|
|
#[\Override]
|
|
protected function get_curl_resolve_info(string $url): array|null|false {
|
|
return FreshRSS_http_Util::getCurlResolveInfo($url);
|
|
}
|
|
|
|
#[\Override]
|
|
protected function on_http_response($response, array $curl_options = []): void {
|
|
if (FreshRSS_Context::systemConf()->simplepie_syslog_enabled) {
|
|
syslog(LOG_INFO, 'FreshRSS SimplePie GET ' . $this->get_status_code() . ' ' . \SimplePie\Misc::url_remove_credentials($this->get_final_requested_uri()));
|
|
}
|
|
|
|
if (in_array($this->get_status_code(), [429, 503], true)) {
|
|
$parser = new \SimplePie\HTTP\Parser(is_string($response) ? $response : '');
|
|
if ($parser->parse()) {
|
|
$headers = $parser->headers;
|
|
} else {
|
|
$headers = [];
|
|
}
|
|
|
|
$proxy = is_string($curl_options[CURLOPT_PROXY] ?? null) ? $curl_options[CURLOPT_PROXY] : '';
|
|
$retryAfter = FreshRSS_http_Util::setRetryAfter($this->get_final_requested_uri(), $proxy, $headers['retry-after'] ?? '');
|
|
if ($retryAfter > 0) {
|
|
$domain = parse_url($this->get_final_requested_uri(), PHP_URL_HOST);
|
|
if (is_string($domain) && $domain !== '') {
|
|
if (is_int($port = parse_url($this->get_final_requested_uri(), PHP_URL_PORT))) {
|
|
$domain .= ':' . $port;
|
|
}
|
|
$errorMessage = 'Will retry after ' . date('c', $retryAfter) . ' for domain `' . $domain . '`';
|
|
Minz_Log::notice($errorMessage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|