Merge pull request #2461 from acelaya-forks/feature/trusted-proxies

Allow trusted proxies to be provided via TRUSTED_PROXIES env var or config option
This commit is contained in:
Alejandro Celaya 2025-07-18 08:32:48 +02:00 committed by GitHub
commit 3a1ce40a49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 55 additions and 35 deletions

View File

@ -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.

View File

@ -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",

View File

@ -80,6 +80,7 @@ return [
Option\Cors\CorsAllowOriginConfigOption::class,
Option\Cors\CorsAllowCredentialsConfigOption::class,
Option\Cors\CorsMaxAgeConfigOption::class,
Option\TrustedProxiesConfigOption::class,
],
'installation_commands' => [

View File

@ -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'],
],
];
];
})();

View File

@ -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];
}

View File

@ -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';

View File

@ -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
{