diff --git a/CHANGELOG.md b/CHANGELOG.md index 383f89a8..a5d1339d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this The new conditions match as soon as a query param exists with any or no value (in the case of `any-value-query-param`), or if a query param exists with no value at all (in the case of `valueless-query-param`). +* [#2387](https://github.com/shlinkio/shlink/issues/2387) Add `TRUSTED_PROXIES` env var and corresponding config option, to configure a comma-separated list of all the proxies in front of Shlink, or simply the amount of trusted proxies in front of Shlink. + + This is important to properly detect visitor's IP addresses instead of incorrectly matching one of the proxy's IP address, and if provided, it disables a workaround introduced in https://github.com/shlinkio/shlink/pull/2359. + ### Changed * [#2406](https://github.com/shlinkio/shlink/issues/2406) Remove references to bootstrap from error templates, and instead inline the very minimum required styles. diff --git a/composer.json b/composer.json index 26031b7c..720a7089 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "shlinkio/shlink-config": "^4.0", "shlinkio/shlink-event-dispatcher": "^4.2", "shlinkio/shlink-importer": "^5.6", - "shlinkio/shlink-installer": "dev-develop#9005232 as 9.6", + "shlinkio/shlink-installer": "dev-develop#eef3749 as 9.6", "shlinkio/shlink-ip-geolocation": "^4.3", "shlinkio/shlink-json": "^1.2", "spiral/roadrunner": "^2025.1", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index e9270eb6..9a7bff04 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -80,6 +80,7 @@ return [ Option\Cors\CorsAllowOriginConfigOption::class, Option\Cors\CorsAllowCredentialsConfigOption::class, Option\Cors\CorsMaxAgeConfigOption::class, + Option\TrustedProxiesConfigOption::class, ], 'installation_commands' => [ diff --git a/config/autoload/ip-address.global.php b/config/autoload/ip-address.global.php index 78f5bc6d..11902091 100644 --- a/config/autoload/ip-address.global.php +++ b/config/autoload/ip-address.global.php @@ -2,49 +2,60 @@ declare(strict_types=1); -use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use RKA\Middleware\IpAddress; use RKA\Middleware\Mezzio\IpAddressFactory; +use Shlinkio\Shlink\Core\Config\EnvVars; use Shlinkio\Shlink\Core\Middleware\ReverseForwardedAddressesMiddlewareDecorator; +use function Shlinkio\Shlink\Core\splitByComma; + use const Shlinkio\Shlink\IP_ADDRESS_REQUEST_ATTRIBUTE; -return [ +return (static function (): array { + $trustedProxies = EnvVars::TRUSTED_PROXIES->loadFromEnv(); + $proxiesIsHopCount = is_numeric($trustedProxies); - // Configuration for RKA\Middleware\IpAddress - 'rka' => [ - 'ip_address' => [ - 'attribute_name' => IP_ADDRESS_REQUEST_ATTRIBUTE, - 'check_proxy_headers' => true, - 'trusted_proxies' => [], - 'headers_to_inspect' => [ - 'CF-Connecting-IP', - 'X-Forwarded-For', - 'X-Forwarded', - 'Forwarded', - 'True-Client-IP', - 'X-Real-IP', - 'X-Cluster-Client-Ip', - 'Client-Ip', + return [ + + // Configuration for RKA\Middleware\IpAddress + 'rka' => [ + 'ip_address' => [ + 'attribute_name' => IP_ADDRESS_REQUEST_ATTRIBUTE, + 'check_proxy_headers' => true, + // List of trusted proxies + 'trusted_proxies' => $proxiesIsHopCount ? [] : splitByComma($trustedProxies), + // Amount of addresses to skip from the right, before finding the visitor IP address + 'hop_count' => $proxiesIsHopCount ? (int) $trustedProxies : 0, + 'headers_to_inspect' => [ + 'CF-Connecting-IP', + 'X-Forwarded-For', + 'X-Forwarded', + 'Forwarded', + 'True-Client-IP', + 'X-Real-IP', + 'X-Cluster-Client-Ip', + 'Client-Ip', + ], ], ], - ], - 'dependencies' => [ - 'factories' => [ -// IpAddress::class => IpAddressFactory::class, - 'actual_ip_address_middleware' => IpAddressFactory::class, - ReverseForwardedAddressesMiddlewareDecorator::class => ConfigAbstractFactory::class, + 'dependencies' => [ + 'factories' => [ + IpAddress::class => IpAddressFactory::class, + ], + 'delegators' => [ + // Make middleware decoration transparent to other parts of the code + IpAddress::class => [ + fn ($c, $n, callable $callback) => + // If trusted proxies have been provided, use original middleware verbatim, otherwise decorate + // with workaround + $trustedProxies !== null + ? $callback() + : new ReverseForwardedAddressesMiddlewareDecorator($callback()), + ], + ], + ], - 'aliases' => [ - // Make sure the decorated middleware is resolved when getting IpAddress::class, to make this decoration - // transparent for other parts of the code - IpAddress::class => ReverseForwardedAddressesMiddlewareDecorator::class, - ], - ], - ConfigAbstractFactory::class => [ - ReverseForwardedAddressesMiddlewareDecorator::class => ['actual_ip_address_middleware'], - ], - -]; + ]; +})(); diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 72406e6a..dd73758f 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -143,6 +143,7 @@ function acceptLanguageToLocales(string $acceptLanguage, float $minQuality = 0): */ function splitLocale(string $locale): array { + /** @var string $lang */ [$lang, $countryCode] = array_pad(explode('-', $locale), length: 2, value: null); return [$lang, $countryCode]; } diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 4f57a721..072ee8db 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -89,6 +89,7 @@ enum EnvVars: string case CORS_ALLOW_ORIGIN = 'CORS_ALLOW_ORIGIN'; case CORS_ALLOW_CREDENTIALS = 'CORS_ALLOW_CREDENTIALS'; case CORS_MAX_AGE = 'CORS_MAX_AGE'; + case TRUSTED_PROXIES = 'TRUSTED_PROXIES'; /** @deprecated Use REDIRECT_EXTRA_PATH */ case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH'; diff --git a/module/Core/src/Middleware/ReverseForwardedAddressesMiddlewareDecorator.php b/module/Core/src/Middleware/ReverseForwardedAddressesMiddlewareDecorator.php index 71d7545c..3a86b129 100644 --- a/module/Core/src/Middleware/ReverseForwardedAddressesMiddlewareDecorator.php +++ b/module/Core/src/Middleware/ReverseForwardedAddressesMiddlewareDecorator.php @@ -26,6 +26,8 @@ use function implode; * if trusted proxies are not set. * * @see https://github.com/akrabat/ip-address-middleware/pull/51 + * @deprecated Remove in future major version, and enforce users with multiple reverse proxies to provide the list via + * TRUSTED_PROXIES */ readonly class ReverseForwardedAddressesMiddlewareDecorator implements MiddlewareInterface {