Skip to content

Loading…

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

Closed
wants to merge 3 commits into from

2 participants

@chillu
SilverStripe Ltd. member

3.0 version. See #1692

CAUTION: Needs to be squashed before merge, leaving unmerged changes in order to illustrate progress and make peer review easier.

@hafriedlander
SilverStripe Ltd. member

Replaced by #2243

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
View
16 cli-script.php
@@ -58,19 +58,15 @@
$_GET['url'] = $_SERVER['argv'][1];
}
-/**
- * Include SilverStripe'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("model/DB.php");
-DB::connect($databaseConfig);
+// Include SilverStripe's core code
+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
15 control/Director.php
@@ -790,6 +790,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')
+ );
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////
// Environment type methods
////////////////////////////////////////////////////////////////////////////////////////////
View
116 core/Core.php
@@ -271,31 +271,43 @@
///////////////////////////////////////////////////////////////////////////////
// MANIFEST
-// Regenerate the manifest if ?flush is set, or if the database is being built.
-// The coupling is a hack, but it removes an annoying bug where new classes
-// referenced in _config.php files can be referenced during the build process.
-$flush = (isset($_GET['flush']) || isset($_REQUEST['url']) && (
- $_REQUEST['url'] == 'dev/build' || $_REQUEST['url'] == BASE_URL . '/dev/build'
-));
-$manifest = new SS_ClassManifest(BASE_PATH, false, $flush);
-
-// Register SilverStripe's class map autoload
-$loader = SS_ClassLoader::instance();
-$loader->registerAutoloader();
-$loader->pushManifest($manifest);
-
-// Fall back to Composer's autoloader (e.g. for PHPUnit), if composer is used
-if(file_exists(BASE_PATH . '/vendor/autoload.php')) {
- require_once BASE_PATH . '/vendor/autoload.php';
-}
-
-// Now that the class manifest is up, load the configuration
-$configManifest = new SS_ConfigManifest(BASE_PATH, false, $flush);
-Config::inst()->pushConfigManifest($configManifest);
+// In case classes are missing, force a flush regardless of authentication or environment type.
+// We can't do this through spl_autoload_register() because some optional classes
+// like Translatable are checked through class_exists(), and are allowed to be missing.
+register_shutdown_function(function() {
+ $error = error_get_last();
+ if(
+ $error && $error['type'] == E_ERROR
+ && preg_match('/(Class|Interface) .* not found/', $error['message'])
+ ) {
+ $loader = SS_ClassLoader::instance();
+ $loader->getManifest()->regenerate();
+ 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();
+ }
+ }
+});
-SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
- BASE_PATH, false, isset($_GET['flush'])
-));
+// 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']));
+loadManifests($isFlush);
// If in live mode, ensure deprecation, strict and notices are not reported
if(Director::isLive()) {
@@ -305,15 +317,63 @@
///////////////////////////////////////////////////////////////////////////////
// POST-MANIFEST COMMANDS
-/**
- * Load error handlers
- */
+// Load error handlers
Debug::loadErrorHandlers();
+// Connect to database
+global $databaseConfig;
+require_once('model/DB.php');
+
+// Throw exception if no database is selected, giving web-based scripts the opportunity
+// to redirect to the installer.
+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()) {
+ loadManifests(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
+/**
+ * @param boolean $isFlush Flush existing caches
+ */
+function loadManifests($isFlush = false) {
+ // Register SilverStripe's class map autoload
+ $manifest = new SS_ClassManifest(BASE_PATH, false, $isFlush);
+ $loader = SS_ClassLoader::instance();
+ $loader->registerAutoloader();
+ $loader->pushManifest($manifest);
+
+ // Fall back to Composer's autoloader (e.g. for PHPUnit), if composer is used
+ if(file_exists(BASE_PATH . '/vendor/autoload.php')) {
+ require_once BASE_PATH . '/vendor/autoload.php';
+ }
+
+ // Now that the class manifest is up, load the configuration
+ $configManifest = new SS_ConfigManifest(BASE_PATH, false, $isFlush);
+ Config::inst()->pushConfigManifest($configManifest);
+
+ SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest(
+ BASE_PATH, false, $isFlush
+ ));
+}
+
function getSysTempDir() {
Deprecation::notice(3.0, 'Please use PHP function get_sys_temp_dir() instead.');
return sys_get_temp_dir();
@@ -488,3 +548,5 @@ function get_increase_time_limit_max() {
global $_increase_time_limit_max;
return $_increase_time_limit_max;
}
+
+class EnvironmentUnconfiguredException extends Exception {}
View
2 dev/install/install.php5
@@ -24,7 +24,7 @@ ini_set('display_errors', 'on');
error_reporting(E_ALL | E_STRICT);
// Attempt to start a session so that the username and password can be sent back to the user.
-if (function_exists('session_start')) {
+if (function_exists('session_start') && !$_SESSION) {
session_start();
}
View
24 docs/en/changelogs/3.0.6.md
@@ -1,5 +1,27 @@
# 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, 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`, technically we only check for the existance
+of any parameter value.
+
## 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 `setValue()` to allow the passing of an object (usually `DataObject`) as well as an array.
View
6 docs/en/reference/urlvariabletools.md
@@ -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. |
- | 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 |
+ | 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: |
## General Testing
View
27 docs/en/topics/caching.md
@@ -0,0 +1,27 @@
+# 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:
+
+ * 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
+
+## Custom Caches
+
+See `[api:SS_Cache]`.
View
1 docs/en/topics/index.md
@@ -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
View
2 i18n/i18n.php
@@ -1957,7 +1957,7 @@ public static function include_locale_file($module, $locale) {
public static function include_by_locale($locale, $clean = false) {
if($clean) {
$cache = Zend_Translate::getCache();
- if($cache) $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
+ if($cache) $cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('Zend_Translate'));
}
// Sort modules by inclusion priority, then alphabetically
View
48 main.php
@@ -56,10 +56,23 @@
*/
-/**
- * Include SilverStripe's core code
- */
-require_once('core/Core.php');
+// Include SilverStripe's core code
+try {
+ require_once('core/Core.php');
+} catch(EnvironmentUnconfiguredException $e) {
+ if(!file_exists(BASE_PATH . '/install.php')) {
+ die('SilverStripe Framework requires a $databaseConfig defined.');
+ }
+ $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();
+}
// IIS will sometimes generate this.
if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
@@ -92,35 +105,8 @@
if (isset($_GET['debug_profile'])) {
Profiler::init();
Profiler::mark('all_execution');
- Profiler::mark('main.php init');
}
-// Connect to database
-require_once('model/DB.php');
-
-// Redirect to the installer if no database is selected
-if(!isset($databaseConfig) || !isset($databaseConfig['database']) || !$databaseConfig['database']) {
- if(!file_exists(BASE_PATH . '/install.php')) {
- die('SilverStripe Framework requires a $databaseConfig defined.');
- }
- $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
DataModel::set_inst(new DataModel());
Director::direct($url, DataModel::inst());
Something went wrong with that request. Please try again.