mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW: Add TrustedProxyMiddleware
API: SS_TRUSTED_PROXY_HOST_HEADER replace with middleware config API: SS_TRUSTED_PROXY_PROTOCOL_HEADER replace with middleware config API: SS_TRUSTED_PROXY_IP_HEADER replace with middleware config API: Front-End-Https = “on” header no longer supported This middleware replaces the TRUSTED_PROXY setting and shifts its configuration out of the env vars and bootstrap and into the Director flow.
This commit is contained in:
parent
c4d038f20d
commit
ccc86306b6
@ -3,6 +3,7 @@ Name: requestprocessors
|
||||
---
|
||||
SilverStripe\Control\Director:
|
||||
middlewares:
|
||||
TrustedProxyMiddleware: '%$SilverStripe\Control\TrustedProxyMiddleware'
|
||||
AllowedHostsMiddleware: '%$SilverStripe\Control\AllowedHostsMiddleware'
|
||||
SessionMiddleware: 'SilverStripe\Control\SessionMiddleware'
|
||||
RequestProcessor: 'SilverStripe\Control\RequestProcessor'
|
||||
@ -13,3 +14,6 @@ SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\Control\AllowedHostsMiddleware:
|
||||
properties:
|
||||
AllowedHosts: "`SS_ALLOWED_HOSTS`"
|
||||
SilverStripe\Control\TrustedProxyMiddleware:
|
||||
properties:
|
||||
TrustedProxyIPs: "`SS_TRUSTED_PROXY_IPS`"
|
||||
|
@ -80,9 +80,6 @@ SilverStripe core environment variables are listed here, though you're free to d
|
||||
| `SS_ERROR_LOG` | Relative path to the log file. |
|
||||
| `SS_PROTECTED_ASSETS_PATH` | Path to secured assets - defaults to ASSET_PATH/.protected |
|
||||
| `SS_DATABASE_MEMORY` | Used for SQLite3 DBs |
|
||||
| `SS_TRUSTED_PROXY_PROTOCOL_HEADER` | Used to define the proxy header to be used to determine HTTPS status |
|
||||
| `SS_TRUSTED_PROXY_IP_HEADER` | Used to define the proxy header to be used to determine request IPs |
|
||||
| `SS_TRUSTED_PROXY_HOST_HEADER` | Used to define the proxy header to be used to determine the requested host name |
|
||||
| `SS_TRUSTED_PROXY_IPS` | IP address or CIDR range to trust proxy headers from. If left blank no proxy headers are trusted. Can be set to 'none' (trust none) or '*' (trust all) |
|
||||
| `SS_ALLOWED_HOSTS` | A comma deliminated list of hostnames the site is allowed to respond to |
|
||||
| `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching). Must be a CacheInterface or CacheFactory class name |
|
||||
|
@ -556,6 +556,18 @@ In order to prevent this kind of attack, it's necessary to whitelist trusted pro
|
||||
server IPs using the SS_TRUSTED_PROXY_IPS define in your `.env`.
|
||||
|
||||
SS_TRUSTED_PROXY_IPS="127.0.0.1,192.168.0.1"
|
||||
|
||||
If you wish to change the headers that are used to find the proxy information, you should reconfigure the
|
||||
TrustedProxyMiddleware service:
|
||||
|
||||
:::yml
|
||||
SilverStripe\Control\TrustedProxyMiddleware:
|
||||
properties:
|
||||
ProxyHostHeaders: X-Forwarded-Host
|
||||
ProxySchemeHeaders: X-Forwarded-Protocol
|
||||
ProxyIPHeaders: X-Forwarded-Ip
|
||||
|
||||
|
||||
SS_TRUSTED_PROXY_HOST_HEADER="HTTP_X_FORWARDED_HOST"
|
||||
SS_TRUSTED_PROXY_IP_HEADER="HTTP_X_FORWARDED_FOR"
|
||||
SS_TRUSTED_PROXY_PROTOCOL_HEADER="HTTP_X_FORWARDED_PROTOCOL"
|
||||
|
@ -1364,6 +1364,9 @@ After (`mysite/_config/config.yml`):
|
||||
* 'BlockUntrustedIPS' env setting has been removed.
|
||||
All IPs are untrusted unless `SS_TRUSTED_PROXY_IPS` is set to '*'
|
||||
See [Environment Management docs](/getting-started/environment_management/) for full details.
|
||||
* `SS_TRUSTED_PROXY_HOST_HEADER`, `SS_TRUSTED_PROXY_PROTOCOL_HEADER`, and `SS_TRUSTED_PROXY_IP_HEADER`
|
||||
are no longer supported. These settings should go into the Injector service configuration for
|
||||
TrustedProxyMiddleware instead.
|
||||
* `MODULES_PATH` removed
|
||||
* `MODULES_DIR` removed
|
||||
* `SS_HOST` removed. Use `SS_BASE_URL` instead.
|
||||
|
@ -384,6 +384,8 @@ class Director implements TemplateGlobalProvider
|
||||
$handler
|
||||
);
|
||||
|
||||
// Note that if a different request was previously registered, this will now be lost
|
||||
// In these cases it's better to use Kernel::nest() prior to kicking off a nested request
|
||||
Injector::inst()->unregisterNamedObject(HTTPRequest::class);
|
||||
|
||||
return $response;
|
||||
@ -518,18 +520,9 @@ class Director implements TemplateGlobalProvider
|
||||
}
|
||||
}
|
||||
|
||||
// Validate proxy-specific headers
|
||||
if (TRUSTED_PROXY) {
|
||||
// Check headers to validate
|
||||
$headers = getenv('SS_TRUSTED_PROXY_HOST_HEADER')
|
||||
? explode(',', getenv('SS_TRUSTED_PROXY_HOST_HEADER'))
|
||||
: ['HTTP_X_FORWARDED_HOST']; // Backwards compatible defaults
|
||||
foreach ($headers as $header) {
|
||||
if (!empty($_SERVER[$header])) {
|
||||
// Get the first host, in case there's multiple separated through commas
|
||||
return strtok($_SERVER[$header], ',');
|
||||
}
|
||||
}
|
||||
$request = Injector::inst()->get(HTTPRequest::class);
|
||||
if ($request && $host = $request->getHeader('Host')) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
// Check given header
|
||||
@ -587,26 +580,10 @@ class Director implements TemplateGlobalProvider
|
||||
}
|
||||
}
|
||||
|
||||
// See https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
|
||||
// See https://support.microsoft.com/en-us/kb/307347
|
||||
if (TRUSTED_PROXY) {
|
||||
$headers = getenv('SS_TRUSTED_PROXY_PROTOCOL_HEADER')
|
||||
? explode(',', getenv('SS_TRUSTED_PROXY_PROTOCOL_HEADER'))
|
||||
: ['HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_PROTOCOL', 'HTTP_FRONT_END_HTTPS'];
|
||||
foreach ($headers as $header) {
|
||||
$headerCompareVal = ($header === 'HTTP_FRONT_END_HTTPS' ? 'on' : 'https');
|
||||
if (!empty($_SERVER[$header]) && strtolower($_SERVER[$header]) == $headerCompareVal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check common $_SERVER
|
||||
if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) {
|
||||
return true;
|
||||
}
|
||||
if (isset($_SERVER['SSL'])) {
|
||||
return true;
|
||||
// Check the current request
|
||||
$request = Injector::inst()->get(HTTPRequest::class);
|
||||
if ($request && $host = $request->getHeader('Host')) {
|
||||
return $request->getScheme() === 'https';
|
||||
}
|
||||
|
||||
// Check default_base_url
|
||||
|
@ -791,29 +791,6 @@ class HTTPRequest implements ArrayAccess
|
||||
$this->ip = $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an IP address from a header value that has been obtained. Accepts single IP or comma separated string of
|
||||
* IPs
|
||||
*
|
||||
* @param string $headerValue The value from a trusted header
|
||||
* @return string The IP address
|
||||
*/
|
||||
protected function getIPFromHeaderValue($headerValue)
|
||||
{
|
||||
if (strpos($headerValue, ',') !== false) {
|
||||
//sometimes the IP from a load balancer could be "x.x.x.x, y.y.y.y, z.z.z.z" so we need to find the most
|
||||
// likely candidate
|
||||
$ips = explode(',', $headerValue);
|
||||
foreach ($ips as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $headerValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all mimetypes from the HTTP "Accept" header
|
||||
* as an array.
|
||||
|
191
src/Control/TrustedProxyMiddleware.php
Normal file
191
src/Control/TrustedProxyMiddleware.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control;
|
||||
|
||||
/**
|
||||
* This middleware will rewrite headers that provide IP and host details from an upstream proxy.
|
||||
*/
|
||||
class TrustedProxyMiddleware implements HTTPMiddleware
|
||||
{
|
||||
|
||||
private $trustedProxyIPs = null;
|
||||
|
||||
private $proxyHostHeaders = [
|
||||
'X-Forwarded-Host'
|
||||
];
|
||||
|
||||
private $proxyIPHeaders = [
|
||||
'Client-IP',
|
||||
'X-Forwarded-For'
|
||||
];
|
||||
|
||||
private $proxySchemeHeaders = [
|
||||
'X-Forwarded-Protocol',
|
||||
'X-Forwarded-Proto',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the comma-separated list of IP ranges that are trusted to provide proxy headers
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTrustedProxyIPs()
|
||||
{
|
||||
return $this->trustedProxyIPs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the comma-separated list of IP ranges that are trusted to provide proxy headers
|
||||
*
|
||||
* @param $trustedProxyIPs string
|
||||
*/
|
||||
public function setTrustedProxyIPs($trustedProxyIPs)
|
||||
{
|
||||
$this->trustedProxyIPs = $trustedProxyIPs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comma-separated list of headers from which to lookup the hostname
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProxyHostHeaders()
|
||||
{
|
||||
return $this->proxyHostHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the comma-separated list of headers from which to lookup the hostname
|
||||
*
|
||||
* @param $proxyHostHeaders string
|
||||
*/
|
||||
public function setProxyHostHeaders($proxyHostHeaders)
|
||||
{
|
||||
$this->proxyHostHeaders = $proxyHostHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comma-separated list of headers from which to lookup the client IP
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProxyIPHeaders()
|
||||
{
|
||||
return $this->proxyIPHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the comma-separated list of headers from which to lookup the client IP
|
||||
*
|
||||
* @param $proxyIPHeaders string
|
||||
*/
|
||||
public function setProxyIPHeaders($proxyIPHeaders)
|
||||
{
|
||||
$this->proxyIPHeaders = $proxyIPHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comma-separated list of headers from which to lookup the client scheme (http/https)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProxySchemeHeaders()
|
||||
{
|
||||
return $this->proxySchemeHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the comma-separated list of headers from which to lookup the client scheme (http/https)
|
||||
*
|
||||
* @param $proxySchemeHeaders string
|
||||
*/
|
||||
public function setProxySchemeHeaders($proxySchemeHeaders)
|
||||
{
|
||||
$this->proxySchemeHeaders = $proxySchemeHeaders;
|
||||
}
|
||||
|
||||
public function process(HTTPRequest $request, callable $delegate)
|
||||
{
|
||||
// If this is a trust proxy
|
||||
if ($this->isTrustedProxy($request)) {
|
||||
// Replace host
|
||||
foreach ($this->proxyHostHeaders as $header) {
|
||||
$hostList = $request->getHeader($header);
|
||||
if ($hostList) {
|
||||
$request->setHeader('Host', strtok($hostList, ','));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace scheme
|
||||
foreach ($this->proxySchemeHeaders as $header) {
|
||||
$scheme = $request->getHeader($header);
|
||||
if ($scheme) {
|
||||
$request->setScheme(strtolower($scheme));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace IP
|
||||
foreach ($this->proxyIPHeaders as $header) {
|
||||
$ipHeader = $this->getIPFromHeaderValue($request->getHeader($header));
|
||||
if ($ipHeader) {
|
||||
$request->setIP($ipHeader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $delegate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current request is coming from a trusted proxy
|
||||
*
|
||||
* @return boolean True if the request's source IP is a trusted proxy
|
||||
*/
|
||||
protected function isTrustedProxy($request)
|
||||
{
|
||||
// Disabled
|
||||
if (empty($this->trustedProxyIPs) || $trustedIPs === 'none') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow all
|
||||
if ($trustedIPs === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate IP address
|
||||
if ($ip = $request->getIP()) {
|
||||
return IPUtils::checkIP($ip, explode(',', $trustedIPs));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an IP address from a header value that has been obtained.
|
||||
* Accepts single IP or comma separated string of IPs
|
||||
*
|
||||
* @param string $headerValue The value from a trusted header
|
||||
* @return string The IP address
|
||||
*/
|
||||
protected function getIPFromHeaderValue($headerValue)
|
||||
{
|
||||
if (strpos($headerValue, ',') !== false) {
|
||||
//sometimes the IP from a load balancer could be "x.x.x.x, y.y.y.y, z.z.z.z" so we need to find the most
|
||||
// likely candidate
|
||||
$ips = explode(',', $headerValue);
|
||||
foreach ($ips as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
return $ip;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $headerValue;
|
||||
}
|
||||
}
|
@ -73,27 +73,6 @@ if (!getenv('SS_IGNORE_DOT_ENV')) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate whether the request comes directly from a trusted server or not
|
||||
* This is necessary to validate whether or not the values of X-Forwarded-
|
||||
* or Client-IP HTTP headers can be trusted
|
||||
*/
|
||||
if (!defined('TRUSTED_PROXY')) {
|
||||
define('TRUSTED_PROXY', call_user_func(function () {
|
||||
$trustedIPs = getenv('SS_TRUSTED_PROXY_IPS');
|
||||
if (empty($trustedIPs) || $trustedIPs === 'none') {
|
||||
return false;
|
||||
}
|
||||
if ($trustedIPs === '*') {
|
||||
return true;
|
||||
}
|
||||
// Validate IP address
|
||||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
return IPUtils::checkIP($_SERVER['REMOTE_ADDR'], explode(',', $trustedIPs));
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
if (!defined('BASE_URL')) {
|
||||
define('BASE_URL', call_user_func(function () {
|
||||
|
Loading…
Reference in New Issue
Block a user