Permalink
Browse files

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

  • Loading branch information...
1 parent 5796ed2 commit 939447040f5556f0ea05902b1762e372a285e32f @chillu chillu committed Jul 17, 2013
Showing with 142 additions and 51 deletions.
  1. +5 −10 cli-script.php
  2. +34 −4 core/Core.php
  3. +52 −10 core/ManifestBuilder.php
  4. +15 −0 core/control/Director.php
  5. +22 −0 docs/en/changelogs/2.4.11.md
  6. +14 −27 main.php
View
15 cli-script.php
@@ -50,19 +50,14 @@
$_REQUEST = $_GET;
}
-/**
- * Include Sapphire's core code
- */
-require_once("core/Core.php");
-
-global $databaseConfig;
-
// We don't have a session in cli-script, but this prevents errors
$_SESSION = null;
-// Connect to database
-require_once("core/model/DB.php");
-DB::connect($databaseConfig);
+try {
+ require_once("core/Core.php");
+} catch(EnvironmentUnconfiguredException $e) {
+ // Ignore warnings on CLI
+}
// Get the request URL from the querystring arguments
$url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : null;
View
38 core/Core.php
@@ -226,10 +226,13 @@ function sapphire_autoload($className) {
///////////////////////////////////////////////////////////////////////////////
// MANIFEST
-/**
- * Include the manifest
- */
-ManifestBuilder::include_manifest();
+register_shutdown_function(array('ManifestBuilder', 'shutdown_handler'));
+
+// Load manifests from caches, or generate them if not present.
+// Will get called again after bootstrap if ?flush=1 is requested (and available).
+// Only allow CLI to flush before bootstrap (and before auth can be checked).
+$isFlush = (php_sapi_name() == 'cli' && isset($_GET['flush']));
+ManifestBuilder::include_manifest($isFlush);
/**
* ?debugmanifest=1 hook
@@ -251,6 +254,31 @@ function sapphire_autoload($className) {
*/
Debug::loadErrorHandlers();
+// Connect to database
+global $databaseConfig;
+require_once("core/model/DB.php");
+
+// Redirect to the installer if no database is selected
+if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseConfig['database']) {
+ throw new EnvironmentUnconfiguredException();
+}
+
+if (isset($_GET['debug_profile'])) Profiler::mark('DB::connect');
+DB::connect($databaseConfig);
+if (isset($_GET['debug_profile'])) Profiler::unmark('DB::connect');
+
+// Now that we've loaded the configuration, determine if caches should be flushed.
+// The manifest is auto-flushed on missing classes by the shutdown function defined in Core.php,
+// so if we've gotten here we can assume all defined classes are available.
+if(isset($_GET['flush'])) {
+ if(Director::can_flush()) {
+ ManifestBuilder::include_manifest(true);
+ } else {
+ if(!headers_sent()) header('Status: 401 Unauthorized');
+ die("Flush not allowed. Either login as admin, or set the environment type to 'dev'");
+ }
+}
+
///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
@@ -414,3 +442,5 @@ function increase_time_limit_to($timeLimit = null) {
}
}
}
+
+class EnvironmentUnconfiguredException extends Exception {}
View
62 core/ManifestBuilder.php
@@ -60,33 +60,42 @@ class ManifestBuilder {
/**
* Include the manifest, regenerating it if necessary
+ *
+ * @param Boolean $flush Force a flush. Even if set to FALSE,
+ * a flush can be triggered if the cache doesn't exist or is stale.
*/
- static function include_manifest() {
+ static function include_manifest($flush = false) {
if(isset($_REQUEST['usetestmanifest'])) {
- self::load_test_manifest();
+ self::load_test_manifest($flush);
} else {
- // The dev/build reference is some coupling but it solves an annoying bug
- if(!file_exists(MANIFEST_FILE) || (filemtime(MANIFEST_FILE) < filemtime(BASE_PATH))
- || isset($_GET['flush']) || (isset($_REQUEST['url']) && ($_REQUEST['url'] == 'dev/build'
- || $_REQUEST['url'] == BASE_URL . '/dev/build'))) {
+ if(
+ $flush
+ || !file_exists(MANIFEST_FILE)
+ || (filemtime(MANIFEST_FILE) < filemtime(BASE_PATH))
+ ) {
self::create_manifest_file();
}
+
require_once(MANIFEST_FILE);
}
}
/**
* Load a copy of the manifest with tests/ folders included.
* Only loads the ClassInfo and __autoload() globals; this assumes that _config.php files are already included.
+ *
+ * @param Boolean $flush Force a flush. Even if set to FALSE,
+ * a flush can be triggered if the cache doesn't exist or is stale.
*/
- static function load_test_manifest() {
+ static function load_test_manifest($flush = false) {
$testManifestFile = MANIFEST_FILE . '-test';
// The dev/build reference is some coupling but it solves an annoying bug
- if(!file_exists($testManifestFile)
+ if(
+ $flush
+ || !file_exists($testManifestFile)
|| (filemtime($testManifestFile) < filemtime(BASE_PATH))
- || isset($_GET['flush'])) {
-
+ ) {
// Build the manifest, including the tests/ folders
$manifestInfo = self::get_manifest_info(BASE_PATH);
$manifest = self::generate_php_file($manifestInfo);
@@ -154,6 +163,39 @@ static function process_manifest($manifestInfo) {
require_once("$requireItem");
}
}
+
+ /**
+ * Passed to register_shutdown_function() in order to respond to E_FATAL errors
+ * about missing classes in a dev-friendly fashion: By preemptively clearing
+ * the manifest before terminating execution.
+ */
+ static function shutdown_handler() {
+ $error = error_get_last();
+ if(
+ $error && $error['type'] == E_ERROR
+ && preg_match('/(Class|Interface) .* not found/', $error['message'])
+ ) {
+ ManifestBuilder::include_manifest(true);
+ if(php_sapi_name() == "cli") {
+ die("Cleared class manifest, please try again\n");
+ } elseif(!isset($_GET['flushed'])) {
+ // Can't redirect through HTTP since headers have already been sent.
+ // flushed=1 guards against infinite loops if the class still can't be found after clearing.
+ echo <<<TXT
+<p><strong>Cleared class manifest.</strong> <span id="refresh">Please refresh</span></p>
+<script>
+ document.getElementById("refresh").innerHTML = "Refreshing...";
+ setTimeout(function() {
+ var url = document.location.href.replace(/flush=[^&]+/,'');
+ url += url.match(/\?/) ? '&flushed=1' : '?flushed=1';
+ document.location.href = url;
+ }, 1000);
+</script>
+TXT;
+ die();
+ }
+ }
+ }
/**
* Get themes from a particular directory.
View
15 core/control/Director.php
@@ -729,6 +729,21 @@ public static function is_cli() {
return (php_sapi_name() == "cli");
}
+ /**
+ * Only operational *after* both class manifest and config information are
+ * in place since it uses the permission system (and database information).
+ *
+ * @return boolean
+ */
+ public static function can_flush() {
+ return (
+ Director::is_cli()
+ || Director::isDev()
+ || !Security::database_is_ready()
+ || Permission::check('ADMIN')
+ );
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////
// Site mode methods
////////////////////////////////////////////////////////////////////////////////////////////
View
22 docs/en/changelogs/2.4.11.md
@@ -0,0 +1,22 @@
+# 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, the action is only executed in the following cases:
+
+ * The [environment](/topics/environment-management) is in "dev mode"
+ * The request is executed through CLI
+ * A user is logged in with ADMIN permissions
+ * A referenced class can't be found in the manifest
+
+This applies to both `flush=1` and `flush=all`.
View
41 main.php
@@ -55,10 +55,20 @@
*/
-/**
- * Include Sapphire's core code
- */
-require_once("core/Core.php");
+// Include Sapphire's core code
+try {
+ require_once("core/Core.php");
+} catch(EnvironmentUnconfiguredException $e) {
+ $s = (isset($_SERVER['SSL']) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) ? 's' : '';
+ $installURL = "http$s://" . $_SERVER['HTTP_HOST'] . BASE_URL . '/install.php';
+
+ // The above dirname() will equate to "\" on Windows when installing directly from http://localhost (not using
+ // a sub-directory), this really messes things up in some browsers. Let's get rid of the backslashes
+ $installURL = str_replace('\\', '', $installURL);
+
+ header("Location: $installURL");
+ die();
+}
if (function_exists('mb_http_output')) {
mb_http_output('UTF-8');
@@ -98,31 +108,8 @@
if (isset($_GET['debug_profile'])) {
Profiler::init();
Profiler::mark('all_execution');
- Profiler::mark('main.php init');
-}
-
-// Connect to database
-require_once("core/model/DB.php");
-
-// Redirect to the installer if no database is selected
-if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseConfig['database']) {
- $s = (isset($_SERVER['SSL']) || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')) ? 's' : '';
- $installURL = "http$s://" . $_SERVER['HTTP_HOST'] . BASE_URL . '/install.php';
-
- // The above dirname() will equate to "\" on Windows when installing directly from http://localhost (not using
- // a sub-directory), this really messes things up in some browsers. Let's get rid of the backslashes
- $installURL = str_replace('\\', '', $installURL);
-
- header("Location: $installURL");
- die();
}
-if (isset($_GET['debug_profile'])) Profiler::mark('DB::connect');
-DB::connect($databaseConfig);
-if (isset($_GET['debug_profile'])) Profiler::unmark('DB::connect');
-
-if (isset($_GET['debug_profile'])) Profiler::unmark('main.php init');
-
// Direct away - this is the "main" function, that hands control to the appropriate controller
Director::direct($url);

0 comments on commit 9394470

Please sign in to comment.