Permalink
Browse files

FIX Prevent DOS by checking for env and admin on ?flush=1 (#1692)

  • Loading branch information...
1 parent d9b0d14 commit 1298d4a5bd927117f9893f32fd02a75ed10d623b Hamish Friedlander committed Jul 18, 2013
@@ -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();
+ }
+ }
+}
@@ -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;
+ }
+}
@@ -1,5 +1,32 @@
# 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
- * 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
@@ -17,10 +17,8 @@ Append the option and corresponding value to your URL in your browser's address
| 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: |
- | ?flush=1 | | | | Flushes the current page and included templates |
- | ?flush=all | | | | Flushes the entire template cache |
## General Testing
View
@@ -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]`.
View
@@ -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.
* [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`
* [Configuring your website](configuration): How to configure the `_config.php` file
* [Controller](controller): The intermediate layer between your templates and the data model
Oops, something went wrong.

0 comments on commit 1298d4a

Please sign in to comment.