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:
Sam Minnee 2017-06-23 17:28:04 +12:00 committed by Damian Mooyman
parent c4d038f20d
commit ccc86306b6
8 changed files with 219 additions and 79 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View File

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