Permalink
Browse files

Merge pull request #2246 from hafriedlander/fix/flush_24

 FIX Prevent DOS by checking for env and admin on ?flush=1 (#1692) in 2.4
  • Loading branch information...
2 parents 5796ed2 + 8990788 commit b774db43fb4e1f3af7ccd603b340cd832ab5a904 @sminnee sminnee committed Jul 19, 2013
Showing with 430 additions and 101 deletions.
  1. +1 −59 core/Core.php
  2. +60 −0 core/TempPath.php
  3. +121 −0 core/startup/ErrorControlChain.php
  4. +113 −0 core/startup/ParameterConfirmationToken.php
  5. +23 −0 docs/en/changelogs/2.4.11.md
  6. +112 −42 main.php
View
@@ -173,6 +173,7 @@ function array_fill_keys($keys,$value) {
/**
* Define the temporary folder if it wasn't defined yet
*/
+require_once('core/TempPath.php');
if(!defined('TEMP_FOLDER')) {
define('TEMP_FOLDER', getTempFolder());
}
@@ -254,65 +255,6 @@ function sapphire_autoload($className) {
///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
-function getSysTempDir() {
- if(function_exists('sys_get_temp_dir')) {
- $sysTmp = sys_get_temp_dir();
- } elseif(isset($_ENV['TMP'])) {
- $sysTmp = $_ENV['TMP'];
- } else {
- $tmpFile = tempnam('adfadsfdas','');
- unlink($tmpFile);
- $sysTmp = dirname($tmpFile);
- }
- return $sysTmp;
-}
-
-/**
- * Returns the temporary folder that sapphire/silverstripe should use for its cache files
- * This is loaded into the TEMP_FOLDER define on start up
- *
- * @param $base The base path to use as the basis for the temp folder name. Defaults to BASE_PATH,
- * which is usually fine; however, the $base argument can be used to help test.
- */
-function getTempFolder($base = null) {
- if(!$base) $base = BASE_PATH;
-
- if($base) {
- $cachefolder = "silverstripe-cache" . str_replace(array(' ', "/", ":", "\\"), "-", $base);
- } else {
- $cachefolder = "silverstripe-cache";
- }
-
- $ssTmp = BASE_PATH . "/silverstripe-cache";
- if(@file_exists($ssTmp)) {
- return $ssTmp;
- }
-
- $sysTmp = getSysTempDir();
- $worked = true;
- $ssTmp = "$sysTmp/$cachefolder";
-
- if(!@file_exists($ssTmp)) {
- @$worked = mkdir($ssTmp);
- }
-
- if(!$worked) {
- $ssTmp = BASE_PATH . "/silverstripe-cache";
- $worked = true;
- if(!@file_exists($ssTmp)) {
- @$worked = mkdir($ssTmp);
- }
- }
-
- if(!$worked) {
- user_error("Permission problem gaining access to a temp folder. " .
- "Please create a folder named silverstripe-cache in the base folder " .
- "of the installation and ensure it has the correct permissions", E_USER_ERROR);
- }
-
- return $ssTmp;
-}
-
/**
* Return the file where that class is stored.
*
View
@@ -0,0 +1,60 @@
+<?php
+
+function getSysTempDir() {
+ if(function_exists('sys_get_temp_dir')) {
+ $sysTmp = sys_get_temp_dir();
+ } elseif(isset($_ENV['TMP'])) {
+ $sysTmp = $_ENV['TMP'];
+ } else {
+ $tmpFile = tempnam('adfadsfdas','');
+ unlink($tmpFile);
+ $sysTmp = dirname($tmpFile);
+ }
+ return $sysTmp;
+}
+
+/**
+ * Returns the temporary folder that sapphire/silverstripe should use for its cache files
+ * This is loaded into the TEMP_FOLDER define on start up
+ *
+ * @param $base The base path to use as the basis for the temp folder name. Defaults to BASE_PATH,
+ * which is usually fine; however, the $base argument can be used to help test.
+ */
+function getTempFolder($base = null) {
+ if(!$base) $base = BASE_PATH;
+
+ if($base) {
+ $cachefolder = "silverstripe-cache" . str_replace(array(' ', "/", ":", "\\"), "-", $base);
+ } else {
+ $cachefolder = "silverstripe-cache";
+ }
+
+ $ssTmp = BASE_PATH . "/silverstripe-cache";
+ if(@file_exists($ssTmp)) {
+ return $ssTmp;
+ }
+
+ $sysTmp = getSysTempDir();
+ $worked = true;
+ $ssTmp = "$sysTmp/$cachefolder";
+
+ if(!@file_exists($ssTmp)) {
+ @$worked = mkdir($ssTmp);
+ }
+
+ if(!$worked) {
+ $ssTmp = BASE_PATH . "/silverstripe-cache";
+ $worked = true;
+ if(!@file_exists($ssTmp)) {
+ @$worked = mkdir($ssTmp);
+ }
+ }
+
+ if(!$worked) {
+ user_error("Permission problem gaining access to a temp folder. " .
+ "Please create a folder named silverstripe-cache in the base folder " .
+ "of the installation and ensure it has the correct permissions", E_USER_ERROR);
+ }
+
+ return $ssTmp;
+}
@@ -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;
+ }
+}
@@ -0,0 +1,23 @@
+# 2.4.11 (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 and ?flush=all
+
+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`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.
Oops, something went wrong.

0 comments on commit b774db4

Please sign in to comment.