Skip to content

Commit

Permalink
Add trusted hosts support to library (#549)
Browse files Browse the repository at this point in the history
Adds support for a new configuration value app.trustedHosts that defines the allowed hosts for the application. This allows a developer to prevent host header poisoning.

Possible values:

true: Trust the host specified in app.url, as well as all subdomains.
false: Disable the trusted hosts feature.
array: Defines the domains to be trusted hosts. Each item should be a string defining a domain, or a regex pattern.

Related: octobercms/october#5423
(cherry picked from commit f29865a)
  • Loading branch information
Ben Thomson authored and Luke Towers committed Jan 4, 2021
1 parent b57093b commit f86fcbc
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 6 deletions.
13 changes: 7 additions & 6 deletions src/Foundation/Http/Kernel.php
Expand Up @@ -10,13 +10,13 @@ class Kernel extends HttpKernel
* @var array
*/
protected $bootstrappers = [
'\October\Rain\Foundation\Bootstrap\RegisterClassLoader',
'\October\Rain\Foundation\Bootstrap\LoadEnvironmentVariables',
'\October\Rain\Foundation\Bootstrap\LoadConfiguration',
'\October\Rain\Foundation\Bootstrap\LoadTranslation',
\October\Rain\Foundation\Bootstrap\RegisterClassLoader::class,
\October\Rain\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\October\Rain\Foundation\Bootstrap\LoadConfiguration::class,
\October\Rain\Foundation\Bootstrap\LoadTranslation::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
'\October\Rain\Foundation\Bootstrap\RegisterOctober',
\October\Rain\Foundation\Bootstrap\RegisterOctober::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
Expand All @@ -27,7 +27,8 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
'\October\Rain\Foundation\Http\Middleware\CheckForMaintenanceMode',
\October\Rain\Foundation\Http\Middleware\CheckForTrustedHost::class,
\October\Rain\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
];

/**
Expand Down
82 changes: 82 additions & 0 deletions src/Foundation/Http/Middleware/CheckForTrustedHost.php
@@ -0,0 +1,82 @@
<?php namespace October\Rain\Foundation\Http\Middleware;

use Config;
use October\Rain\Http\Middleware\TrustHosts as BaseMiddleware;

class CheckForTrustedHost extends BaseMiddleware
{
/**
* Get the host patterns that should be trusted.
*
* Trusted hosts should be defined in the `config/app.php` configuration file as an array, ie.:
*
* 'trustedHosts' => [
* 'example.com', // Matches just example.com
* 'www.example.com', // Matches just www.example.com
* '^(.+\.)?example\.com$', // Matches example.com and all subdomains
* 'https://example.com', // Matches just example.com
* ],
*
* or as a boolean - if true, it will trust the `app.url` host and all subdomains, if false it
* will disable the feature entirely.
*
* Hosts can be defined as regex patterns for complex matching.
*
* @return array
*/
public function hosts()
{
return self::processTrustedHosts(Config::get('app.trustedHosts', []));
}

/**
* Processes the trusted hosts into an array of patterns the match for host header checks.
*
* @param array|bool $hosts
* @return array
*/
public static function processTrustedHosts($hosts)
{
if ($hosts === true) {
$url = Config::get('app.url', null);

// If no app URL is set, then disable trusted hosts.
if (is_null($url)) {
return [];
}

// Allow both the domain and the `www` subdomain for app.url
// regardless of the presence of www in the app.url value
$host = parse_url($url, PHP_URL_HOST);
if (preg_match('/^www\.(.*?)$/i', $host, $matches)) {
$host = '^(www\.)?' . preg_quote($matches[1]) . '$';
} else {
$host = '^(www\.)?' . preg_quote($host) . '$';
}

$hosts = [$host];
} elseif ($hosts === false) {
return [];
}

$hosts = array_map(function ($host) {
// If a URL is provided, extract the host
if (filter_var($host, FILTER_VALIDATE_URL)) {
$host = parse_url($host, PHP_URL_HOST);
}

// Prepare IP address & plain hostname values to be processed by the regex filter
if (
filter_var($host, FILTER_VALIDATE_IP)
|| filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)
) {
return '^' . preg_quote($host) . '$';
}

// Allow everything else through as is
return $host;
}, $hosts);

return $hosts;
}
}
71 changes: 71 additions & 0 deletions src/Http/Middleware/TrustHosts.php
@@ -0,0 +1,71 @@
<?php namespace October\Rain\Http\Middleware;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;

abstract class TrustHosts
{
/**
* The application instance.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;

/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct(Application $app)
{
$this->app = $app;
}

/**
* Get the host patterns that should be trusted.
*
* @return array
*/
abstract public function hosts();

/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param callable $next
* @return \Illuminate\Http\Response
*/
public function handle(Request $request, $next)
{
if ($this->shouldSpecifyTrustedHosts()) {
Request::setTrustedHosts(array_filter($this->hosts()));
}

return $next($request);
}

/**
* Determine if the application should specify trusted hosts.
*
* @return bool
*/
protected function shouldSpecifyTrustedHosts()
{
return $this->app['config']->get('app.env') !== 'local'
&& $this->app->runningUnitTests() === false;
}

/**
* Get a regular expression matching the application URL and all of its subdomains.
*
* @return string|null
*/
protected function allSubdomainsOfApplicationUrl()
{
if ($host = parse_url($this->app['config']->get('app.url'), PHP_URL_HOST)) {
return '^(.+\.)?'.preg_quote($host).'$';
}
}
}

0 comments on commit f86fcbc

Please sign in to comment.