Skip to content

Commit

Permalink
FIX Prevent DOS by checking for env and admin on ?flush=1 (silverstri…
Browse files Browse the repository at this point in the history
  • Loading branch information
Hamish Friedlander committed Jul 19, 2013
1 parent d9b0d14 commit 1298d4a
Show file tree
Hide file tree
Showing 8 changed files with 484 additions and 49 deletions.
121 changes: 121 additions & 0 deletions core/startup/ErrorControlChain.php
@@ -0,0 +1,121 @@
<?php

/**
* Class ErrorControlChain
*
* Runs a set of steps, optionally suppressing (but recording) any errors (even fatal ones) that occur in each step.
* If an error does occur, subsequent steps are normally skipped, but can optionally be run anyway
*
* Normal errors are suppressed even past the end of the chain. Fatal errors are only suppressed until the end
* of the chain - the request will then die silently.
*
* The exception is if an error occurs and BASE_URL is not yet set - in that case the error is never suppressed.
*
* Usage:
*
* $chain = new ErrorControlChain();
* $chain->then($callback1)->then($callback2)->then(true, $callback3)->execute();
*
* WARNING: This class is experimental and designed specifically for use pre-startup in main.php
* It will likely be heavily refactored before the release of 3.2
*/
class ErrorControlChain {
protected $error = false;
protected $steps = array();

protected $suppression = true;

/** We can't unregister_shutdown_function, so this acts as a flag to enable handling */
protected $handleFatalErrors = false;

public function hasErrored() {
return $this->error;
}

public function setErrored($error) {
$this->error = (bool)$error;
}

public function setSuppression($suppression) {
$this->suppression = (bool)$suppression;
}

/**
* Add this callback to the chain of callbacks to call along with the state
* that $error must be in this point in the chain for the callback to be called
*
* @param $callback - The callback to call
* @param $onErrorState - false if only call if no errors yet, true if only call if already errors, null for either
* @return $this
*/
public function then($callback, $onErrorState = false) {
$this->steps[] = array(
'callback' => $callback,
'onErrorState' => $onErrorState
);
return $this;
}

public function thenWhileGood($callback) {
return $this->then($callback, false);
}

public function thenIfErrored($callback) {
return $this->then($callback, true);
}

public function thenAlways($callback) {
return $this->then($callback, null);
}

public function handleError() {
if ($this->suppression && defined('BASE_URL')) throw new Exception('Generic Error');
else return false;
}

protected function lastErrorWasFatal() {
$error = error_get_last();
return $error && $error['type'] == 1;
}

public function handleFatalError() {
if ($this->handleFatalErrors && $this->suppression && defined('BASE_URL')) {
if ($this->lastErrorWasFatal()) {
ob_clean();
$this->error = true;
$this->step();
}
}
}

public function execute() {
set_error_handler(array($this, 'handleError'), error_reporting());
register_shutdown_function(array($this, 'handleFatalError'));
$this->handleFatalErrors = true;

$this->step();
}

protected function step() {
if ($this->steps) {
$step = array_shift($this->steps);

if ($step['onErrorState'] === null || $step['onErrorState'] === $this->error) {
try {
call_user_func($step['callback'], $this);
}
catch (Exception $e) {
if ($this->suppression && defined('BASE_URL')) $this->error = true;
else throw $e;
}
}

$this->step();
}
else {
// Now clean up
$this->handleFatalErrors = false;
restore_error_handler();
}
}
}
113 changes: 113 additions & 0 deletions core/startup/ParameterConfirmationToken.php
@@ -0,0 +1,113 @@
<?php

/**
* Class ParameterConfirmationToken
*
* When you need to use a dangerous GET parameter that needs to be set before core/Core.php is
* established, this class takes care of allowing some other code of confirming the parameter,
* by generating a one-time-use token & redirecting with that token included in the redirected URL
*
* WARNING: This class is experimental and designed specifically for use pre-startup in main.php
* It will likely be heavily refactored before the release of 3.2
*/
class ParameterConfirmationToken {
protected $parameterName = null;
protected $parameter = null;
protected $token = null;

protected function pathForToken($token) {
if (defined('BASE_PATH')) {
$basepath = BASE_PATH;
}
else {
$basepath = rtrim(dirname(dirname(dirname(dirname(__FILE__)))), DIRECTORY_SEPARATOR);
}

require_once('core/TempPath.php');
$tempfolder = getTempFolder($basepath ? $basepath : DIRECTORY_SEPARATOR);

return $tempfolder.'/token_'.preg_replace('/[^a-z0-9]+/', '', $token);
}

protected function genToken() {
// Generate a new random token (as random as possible)
require_once('security/RandomGenerator.php');
$rg = new RandomGenerator();
$token = $rg->randomToken('md5');

// Store a file in the session save path (safer than /tmp, as open_basedir might limit that)
file_put_contents($this->pathForToken($token), $token);

return $token;
}

protected function checkToken($token) {
$file = $this->pathForToken($token);
$content = null;

if (file_exists($file)) {
$content = file_get_contents($file);
unlink($file);
}

return $content == $token;
}

public function __construct($parameterName) {
// Store the parameter name
$this->parameterName = $parameterName;
// Store the parameter value
$this->parameter = isset($_GET[$parameterName]) ? $_GET[$parameterName] : null;
// Store the token
$this->token = isset($_GET[$parameterName.'token']) ? $_GET[$parameterName.'token'] : null;

// If a token was provided, but isn't valid, just throw a 403
if ($this->token && (!$this->checkToken($this->token))) {
header("HTTP/1.0 403 Forbidden", true, 403);
die;
}
}

public function parameterProvided() {
return $this->parameter !== null;
}

public function tokenProvided() {
return $this->token !== null;
}

public function params() {
return array(
$this->parameterName => $this->parameter,
$this->parameterName.'token' => $this->genToken()
);
}

public function reloadWithToken() {
global $url;

// Are we http or https?
$proto = 'http';

if(isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])) {
if(strtolower($_SERVER['HTTP_X_FORWARDED_PROTOCOL']) == 'https') $proto = 'https';
}

if((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) $proto = 'https';
if(isset($_SERVER['SSL'])) $proto = 'https';

// What's our host
$host = $_SERVER['HTTP_HOST'];

// What's our GET params (ensuring they include the original parameter + a new token)
$params = array_merge($_GET, $this->params());
unset($params['url']);

// Join them all together into the original URL
$location = "$proto://" . $host . BASE_URL . $url . ($params ? '?'.http_build_query($params) : '');

// And redirect
header('location: '.$location, true, 302);
die;
}
}
29 changes: 28 additions & 1 deletion docs/en/changelogs/3.0.6.md
@@ -1,5 +1,32 @@
# 3.0.6 (Not yet released) # 3.0.6 (Not yet released)


## Overview

* Security: Require ADMIN for `?flush=1` (stop denial of service attacks)
([#1692](https://github.com/silverstripe/silverstripe-framework/issues/1692))

## Details

### Security: Require ADMIN for ?flush=1

Flushing the various manifests (class, template, config) is performed through a GET
parameter (`flush=1`). Since this action requires more server resources than normal requests,
it can facilitate [denial-of-service attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack).

To prevent this, main.php now checks and only allows the flush parameter in the following cases:

* The [environment](/topics/environment-management) is in "dev mode"
* A user is logged in with ADMIN permissions
* An error occurs during startup

This applies to both `flush=1` and `flush=all` (technically we only check for the existence of any parameter value)
but only through web requests made through main.php - CLI requests, or any other request that goes through
a custom start up script will still process all flush requests as normal.

## Upgrading ## Upgrading


* If you have created your own composite database fields, then you shoulcd amend the setValue() to allow the passing of an object (usually DataObject) as well as an array. * If you have created your own composite database fields, then you should amend the setValue() to allow the passing of
an object (usually DataObject) as well as an array.

* If you have provided your own startup scripts (ones that include core/Core.php) that can be accessed via a web
request, you should ensure that you limit use of the flush parameter
4 changes: 1 addition & 3 deletions docs/en/reference/urlvariabletools.md
Expand Up @@ -17,10 +17,8 @@ Append the option and corresponding value to your URL in your browser's address


| URL Variable | | Values | | Description | | URL Variable | | Values | | Description |
| ------------ | | ------ | | ----------- | | ------------ | | ------ | | ----------- |
| flush | | 1,all | | This will clear out all cached information about the page. This is used frequently during development - for example, when adding new PHP or SS files. See below for value descriptions. | | flush=1 | | 1 | | Clears out all caches. Used mainly during development, e.g. when adding new classes or templates. Requires "dev" mode or ADMIN login |
| showtemplate | | 1 | | Show the compiled version of all the templates used, including line numbers. Good when you have a syntax error in a template. Cannot be used on a Live site without **isDev**. **flush** can be used with the following values: | | showtemplate | | 1 | | Show the compiled version of all the templates used, including line numbers. Good when you have a syntax error in a template. Cannot be used on a Live site without **isDev**. **flush** can be used with the following values: |
| ?flush=1 | | | | Flushes the current page and included templates |
| ?flush=all | | | | Flushes the entire template cache |


## General Testing ## General Testing


Expand Down
26 changes: 26 additions & 0 deletions docs/en/topics/caching.md
@@ -0,0 +1,26 @@
# Caching

## Built-In Caches

The framework uses caches to store infrequently changing values.
By default, the storage mechanism is simply the filesystem, although
other cache backends can be configured. All caches use the `[api:SS_Cache]` API.

The most common caches are manifests of various resources:

* PHP class locations (`[api:SS_ClassManifest]`)
* Template file locations and compiled templates (`[api:SS_TemplateManifest]`)
* Configuration settings from YAML files (`[api:SS_ConfigManifest]`)
* Language files (`[api:i18n]`)

Flushing the various manifests is performed through a GET
parameter (`flush=1`). Since this action requires more server resources than normal requests,
executing the action is limited to the following cases when performed via a web request:

* The [environment](/topics/environment-management) is in "dev mode"
* A user is logged in with ADMIN permissions
* An error occurs during startup

## Custom Caches

See `[api:SS_Cache]`.
1 change: 1 addition & 0 deletions docs/en/topics/index.md
Expand Up @@ -4,6 +4,7 @@ This section provides an overview on how things fit together, the "conceptual gl
It is where most documentation should live, and is the natural "second step" after finishing the tutorials. It is where most documentation should live, and is the natural "second step" after finishing the tutorials.


* [Access Control and Page Security](access-control): Restricting access and setting up permissions on your website * [Access Control and Page Security](access-control): Restricting access and setting up permissions on your website
* [Caching](caching): Explains built-in caches for classes, config and templates. How to use your own caches.
* [Command line Usage](commandline): Calling controllers via the command line interface using `sake` * [Command line Usage](commandline): Calling controllers via the command line interface using `sake`
* [Configuring your website](configuration): How to configure the `_config.php` file * [Configuring your website](configuration): How to configure the `_config.php` file
* [Controller](controller): The intermediate layer between your templates and the data model * [Controller](controller): The intermediate layer between your templates and the data model
Expand Down

0 comments on commit 1298d4a

Please sign in to comment.