Skip to content

Commit

Permalink
created HostControl filter to prevent Host header attacks
Browse files Browse the repository at this point in the history
  • Loading branch information
klimov-paul authored and cebe committed Nov 30, 2016
1 parent 0a9ffc0 commit 7da77c3
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 0 deletions.
20 changes: 20 additions & 0 deletions docs/guide/security-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,23 @@ For more information about the server configuration, please refer to the documen

- Apache 2: <http://httpd.apache.org/docs/trunk/vhosts/examples.html#defaultallports>
- Nginx: <https://www.nginx.com/resources/wiki/start/topics/examples/server_blocks/>

If you don't have access to the server configuration, you can setup [[yii\filters\HostControl]] filter at
application level in order to protect against such kind of attack:

```php
// Web Application configuration file
return [
'as hostControl' => [
'class' => 'yii\filters\HostControl',
'allowedHosts' => [
'example.com',
'*.example.com',
],
],
// ...
];
```

> Note: you should always prefer web server configuration for 'host header attack' protection instead of the filter usage.
[[yii\filters\HostControl]] should be used only if server configuration setup is unavailable.
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Yii Framework 2 Change Log
- Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar)
- Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333)
- Enh #13074: Improved `\yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks)
- Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul)
- Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe)


Expand Down
164 changes: 164 additions & 0 deletions framework/filters/HostControl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/

namespace yii\filters;

use Yii;
use yii\base\ActionFilter;
use yii\web\NotFoundHttpException;

/**
* HostControl provides simple control over requested host name.
*
* This filter provides protection against ['host header' attacks](https://www.acunetix.com/vulnerabilities/web/host-header-attack),
* allowing action execution only for specified host names.
*
* Application configuration example:
*
* ```php
* return [
* 'as hostControl' => [
* 'class' => 'yii\filters\HostControl',
* 'allowedHosts' => [
* 'example.com',
* '*.example.com',
* ],
* ],
* // ...
* ];
* ```
*
* Controller configuration example:
*
* ```php
* use yii\web\Controller;
* use yii\filters\HostControl;
*
* class SiteController extends Controller
* {
* public function behaviors()
* {
* return [
* 'hostControl' => [
* 'class' => HostControl::className(),
* 'allowedHosts' => [
* 'example.com',
* '*.example.com',
* ],
* ],
* ];
* }
*
* // ...
* }
* ```
*
* > Note: the best way to restrict allowed host names is usage of the web server 'virtual hosts' configuration.
* This filter should be used only if this configuration is not available or compromised.
*
* @author Paul Klimov <[email protected]>
* @since 2.0.11
*/
class HostControl extends ActionFilter
{
/**
* @var array|\Closure|null list of host names, which are allowed.
* Each host can be specified as a wildcard pattern. For example:
*
* ```php
* [
* 'example.com',
* '*.example.com',
* ]
* ```
*
* This field can be specified as a PHP callback of following signature:
*
* ```php
* function (\yii\base\Action $action) {
* //return array of strings
* }
* ```
*
* where `$action` is the current [[\yii\base\Action|action]] object.
*
* If this field is not set - no host name check will be performed.
*/
public $allowedHosts;
/**
* @var callable a callback that will be called if the current host does not match [[allowedHosts]].
* If not set, [[denyAccess()]] will be called.
*
* The signature of the callback should be as follows:
*
* ```php
* function (\yii\base\Action $action)
* ```
*
* where `$action` is the current [[\yii\base\Action|action]] object.
*
* > Note: while implementing your own host deny processing, make sure you avoid usage of the current requested
* host name, creation of absolute URL links, caching page parts and so on.
*/
public $denyCallback;


/**
* @inheritdoc
*/
public function beforeAction($action)
{
$allowedHosts = $this->allowedHosts;
if ($allowedHosts instanceof \Closure) {
$allowedHosts = call_user_func($allowedHosts, $action);
}
if ($allowedHosts === null) {
return true;
}

if (!is_array($allowedHosts) && !$allowedHosts instanceof \Traversable) {
$allowedHosts = (array)$allowedHosts;
}

$currentHost = Yii::$app->getRequest()->getHostName();

foreach ($allowedHosts as $allowedHost) {
if (fnmatch($allowedHost, $currentHost)) {
return true;
}
}

if ($this->denyCallback !== null) {
call_user_func($this->denyCallback, $action);
} else {
$this->denyAccess($action);
}

return false;
}

/**
* Denies the access.
* The default implementation will display 404 page right away, terminating the program execution.
* You may override this method, creating your own deny access handler. While doing so, make sure you
* avoid usage of the current requested host name, creation of absolute URL links, caching page parts and so on.
* @param \yii\base\Action $action the action to be executed.
*/
protected function denyAccess($action)
{
$response = Yii::$app->getResponse();
$errorHandler = Yii::$app->getErrorHandler();

$exception = new NotFoundHttpException(Yii::t('yii', 'Page not found.'));

$response->setStatusCode($exception->statusCode, $exception->getMessage());
$response->data = $errorHandler->renderFile($errorHandler->errorView, ['exception' => $exception]);
$response->send();

Yii::$app->end();
}
}
6 changes: 6 additions & 0 deletions framework/web/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,8 @@ public function getQueryParam($name, $defaultValue = null)
* > If the webserver is configured to serve the same site independent of the value of
* > the `Host` header, this value is not reliable. In such situations you should either
* > fix your webserver configuration or explicitly set the value by setting the [[setHostInfo()|hostInfo]] property.
* > If you don't have access to the server configuration, you can setup [[\yii\filters\HostControl]] filter at
* > application level in order to protect against such kind of attack.
*
* @property string|null schema and hostname part (with port number if needed) of the request URL
* (e.g. `http://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set.
Expand Down Expand Up @@ -587,6 +589,10 @@ public function setHostInfo($value)
/**
* Returns the host part of the current request URL.
* Value is calculated from current [[getHostInfo()|hostInfo]] property.
*
* > Warning: The content of this value may not be reliable, dependent on the server
* > configuration. Please refer to [[getHostInfo()]] for more information.
*
* @return string|null hostname part of the request URL (e.g. `www.yiiframework.com`)
* @see getHostInfo()
* @since 2.0.10
Expand Down
1 change: 1 addition & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

define('YII_ENABLE_ERROR_HANDLER', false);
define('YII_DEBUG', true);
define('YII_ENV', 'test');
$_SERVER['SCRIPT_NAME'] = '/' . __DIR__;
$_SERVER['SCRIPT_FILENAME'] = __FILE__;

Expand Down
104 changes: 104 additions & 0 deletions tests/framework/filters/HostControlTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace yiiunit\framework\filters;

use Yii;
use yii\base\Action;
use yii\base\ExitException;
use yii\filters\HostControl;
use yii\web\Controller;
use yiiunit\TestCase;

class HostControlTest extends TestCase
{
protected function setUp()
{
parent::setUp();

$_SERVER['SCRIPT_FILENAME'] = "/index.php";
$_SERVER['SCRIPT_NAME'] = "/index.php";

$this->mockWebApplication();
}

/**
* @return array test data.
*/
public function dataProviderFilter()
{
return [
[
['example.com'],
'example.com',
true
],
[
['example.com'],
'domain.com',
false
],
[
['*.example.com'],
'en.example.com',
true
],
[
['*.example.com'],
'fake.com',
false
],
[
function () {
return ['example.com'];
},
'example.com',
true
],
[
function () {
return ['example.com'];
},
'fake.com',
false
],
];
}

/**
* @dataProvider dataProviderFilter
*
* @param mixed $allowedHosts
* @param string $host
* @param bool $allowed
*/
public function testFilter($allowedHosts, $host, $allowed)
{
$_SERVER['HTTP_HOST'] = $host;

$filter = new HostControl();
$filter->allowedHosts = $allowedHosts;

$controller = new Controller('id', Yii::$app);
$action = new Action('test', $controller);

if ($allowed) {
$this->assertTrue($filter->beforeAction($action));
} else {
ob_start();
ob_implicit_flush(false);

$isExit = false;

try {
$filter->beforeAction($action);
} catch (ExitException $e) {
$isExit = true;
}

ob_get_clean();

$this->assertTrue($isExit);
$this->assertEquals(404, Yii::$app->response->getStatusCode());
}
}
}

0 comments on commit 7da77c3

Please sign in to comment.