Commit b8ef3c6b authored by webchick's avatar webchick

Issue #2140433 by pwolanin, chx, Owen Barton, dstol, Heine, Damien Tournoud:...

Issue #2140433 by pwolanin, chx, Owen Barton, dstol, Heine, Damien Tournoud: Port SA-CORE-2013-003 to Drupal 8.
parent 08a9a799
...@@ -2024,7 +2024,7 @@ function drupal_generate_test_ua($prefix) { ...@@ -2024,7 +2024,7 @@ function drupal_generate_test_ua($prefix) {
else { else {
// Generate and save a new hash salt for a test run. // Generate and save a new hash salt for a test run.
// Consumed by drupal_valid_test_ua() before settings.php is loaded. // Consumed by drupal_valid_test_ua() before settings.php is loaded.
$private_key = Crypt::randomStringHashed(55); $private_key = Crypt::randomBytesBase64(55);
file_put_contents($key_file, $private_key); file_put_contents($key_file, $private_key);
} }
// The file properties add more entropy not easily accessible to others. // The file properties add more entropy not easily accessible to others.
......
<?php <?php
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Json; use Drupal\Component\Utility\Json;
use Drupal\Component\Utility\Number; use Drupal\Component\Utility\Number;
use Drupal\Component\Utility\Settings; use Drupal\Component\Utility\Settings;
...@@ -3195,6 +3196,10 @@ function _drupal_bootstrap_code() { ...@@ -3195,6 +3196,10 @@ function _drupal_bootstrap_code() {
// Make sure all stream wrappers are registered. // Make sure all stream wrappers are registered.
file_get_stream_wrappers(); file_get_stream_wrappers();
// Ensure mt_rand() is reseeded to prevent random values from one page load
// being exploited to predict random values in subsequent page loads.
$seed = unpack("L", Crypt::randomBytes(4));
mt_srand($seed[1]);
// Set the allowed protocols once we have the config available. // Set the allowed protocols once we have the config available.
$allowed_protocols = \Drupal::config('system.filter')->get('protocols'); $allowed_protocols = \Drupal::config('system.filter')->get('protocols');
...@@ -4294,7 +4299,7 @@ function drupal_pre_render_render_cache_placeholder($element) { ...@@ -4294,7 +4299,7 @@ function drupal_pre_render_render_cache_placeholder($element) {
if (!is_array($context)) { if (!is_array($context)) {
throw new Exception(t('#context must be an array.')); throw new Exception(t('#context must be an array.'));
} }
$token = \Drupal\Component\Utility\Crypt::randomStringHashed(55); $token = \Drupal\Component\Utility\Crypt::randomBytesBase64(55);
// Generate placeholder markup and store #post_render_cache callback. // Generate placeholder markup and store #post_render_cache callback.
$element['#markup'] = drupal_render_cache_generate_placeholder($callback, $context, $token); $element['#markup'] = drupal_render_cache_generate_placeholder($callback, $context, $token);
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
*/ */
use Drupal\Core\StreamWrapper\LocalStream; use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage; use Drupal\Component\PhpStorage\FileStorage;
use Drupal\Component\Utility\String; use Drupal\Component\Utility\String;
use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\StreamWrapper\PublicStream;
...@@ -579,17 +579,16 @@ function file_ensure_htaccess() { ...@@ -579,17 +579,16 @@ function file_ensure_htaccess() {
/** /**
* Creates a .htaccess file in the given directory. * Creates a .htaccess file in the given directory.
* *
* @param $directory * @param string $directory
* The directory. * The directory.
* @param $private * @param bool $private
* FALSE indicates that $directory should be an open and public directory. * (Optional) FALSE indicates that $directory should be a web-accessible
* The default is TRUE which indicates a private and protected directory. * directory. Defaults to TRUE which indicates a private directory.
* * @param bool $force_overwrite
* @return bool * (Optional) Set to TRUE to attempt to overwrite the existing .htaccess file
* TRUE if the .htaccess file could be created or existed already, FALSE * if one is already present. Defaults to FALSE.
* otherwise. */
*/ function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALSE) {
function file_save_htaccess($directory, $private = TRUE) {
if (file_uri_scheme($directory)) { if (file_uri_scheme($directory)) {
$htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess'); $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
} }
...@@ -598,19 +597,11 @@ function file_save_htaccess($directory, $private = TRUE) { ...@@ -598,19 +597,11 @@ function file_save_htaccess($directory, $private = TRUE) {
$htaccess_path = $directory . '/.htaccess'; $htaccess_path = $directory . '/.htaccess';
} }
if (file_exists($htaccess_path)) { if (file_exists($htaccess_path) && !$force_overwrite) {
// Short circuit if the .htaccess file already exists. // Short circuit if the .htaccess file already exists.
return TRUE; return TRUE;
} }
$htaccess_lines = file_htaccess_lines($private);
if ($private) {
// Private .htaccess file.
$htaccess_lines = MTimeProtectedFastFileStorage::HTACCESS;
}
else {
// Public .htaccess file.
$htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
}
// Write the .htaccess file. // Write the .htaccess file.
if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) { if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) {
...@@ -623,6 +614,23 @@ function file_save_htaccess($directory, $private = TRUE) { ...@@ -623,6 +614,23 @@ function file_save_htaccess($directory, $private = TRUE) {
} }
} }
/**
* Returns the standard .htaccess lines that Drupal writes to file directories.
*
* @param bool $private
* (Optional) Set to FALSE to return the .htaccess lines for a web-accessible
* public directory. The default is TRUE, which returns the .htaccess lines
* for a private directory that should not be web-accessible.
*
* @return string
* The desired contents of the .htaccess file.
*
* @see file_create_htaccess()
*/
function file_htaccess_lines($private = TRUE) {
return FileStorage::htaccessLines($private);
}
/** /**
* Determines whether the URI has a valid scheme for file API operations. * Determines whether the URI has a valid scheme for file API operations.
* *
......
...@@ -1255,7 +1255,7 @@ function install_settings_form_submit($form, &$form_state) { ...@@ -1255,7 +1255,7 @@ function install_settings_form_submit($form, &$form_state) {
'required' => TRUE, 'required' => TRUE,
); );
$settings['settings']['hash_salt'] = (object) array( $settings['settings']['hash_salt'] = (object) array(
'value' => Crypt::randomStringHashed(55), 'value' => Crypt::randomBytesBase64(55),
'required' => TRUE, 'required' => TRUE,
); );
// Remember the profile which was used. // Remember the profile which was used.
......
...@@ -457,7 +457,7 @@ function drupal_install_config_directories() { ...@@ -457,7 +457,7 @@ function drupal_install_config_directories() {
// Add a randomized config directory name to settings.php, unless it was // Add a randomized config directory name to settings.php, unless it was
// manually defined in the existing already. // manually defined in the existing already.
if (empty($config_directories)) { if (empty($config_directories)) {
$config_directories_hash = Crypt::randomStringHashed(55); $config_directories_hash = Crypt::randomBytesBase64(55);
$settings['config_directories'] = array( $settings['config_directories'] = array(
CONFIG_ACTIVE_DIRECTORY => (object) array( CONFIG_ACTIVE_DIRECTORY => (object) array(
'value' => conf_path() . '/files/config_' . $config_directories_hash . '/active', 'value' => conf_path() . '/files/config_' . $config_directories_hash . '/active',
......
...@@ -267,10 +267,10 @@ function drupal_session_initialize() { ...@@ -267,10 +267,10 @@ function drupal_session_initialize() {
// Less random sessions (which are much faster to generate) are used for // Less random sessions (which are much faster to generate) are used for
// anonymous users than are generated in drupal_session_regenerate() when // anonymous users than are generated in drupal_session_regenerate() when
// a user becomes authenticated. // a user becomes authenticated.
session_id(Crypt::hashBase64(uniqid(mt_rand(), TRUE))); session_id(Crypt::randomBytesBase64());
if ($is_https && settings()->get('mixed_mode_sessions', FALSE)) { if ($is_https && settings()->get('mixed_mode_sessions', FALSE)) {
$insecure_session_name = substr(session_name(), 1); $insecure_session_name = substr(session_name(), 1);
$session_id = Crypt::hashBase64(uniqid(mt_rand(), TRUE)); $session_id = Crypt::randomBytesBase64();
$cookies->set($insecure_session_name, $session_id); $cookies->set($insecure_session_name, $session_id);
} }
} }
...@@ -369,7 +369,7 @@ function drupal_session_regenerate() { ...@@ -369,7 +369,7 @@ function drupal_session_regenerate() {
$old_insecure_session_id = $cookies->get($insecure_session_name); $old_insecure_session_id = $cookies->get($insecure_session_name);
} }
$params = session_get_cookie_params(); $params = session_get_cookie_params();
$session_id = Crypt::hashBase64(uniqid(mt_rand(), TRUE) . Crypt::randomBytes(55)); $session_id = Crypt::randomBytesBase64();
// If a session cookie lifetime is set, the session will expire // If a session cookie lifetime is set, the session will expire
// $params['lifetime'] seconds from the current request. If it is not set, // $params['lifetime'] seconds from the current request. If it is not set,
// it will expire when the browser is closed. // it will expire when the browser is closed.
...@@ -381,7 +381,7 @@ function drupal_session_regenerate() { ...@@ -381,7 +381,7 @@ function drupal_session_regenerate() {
if (drupal_session_started()) { if (drupal_session_started()) {
$old_session_id = session_id(); $old_session_id = session_id();
} }
session_id(Crypt::hashBase64(uniqid(mt_rand(), TRUE) . Crypt::randomBytes(55))); session_id(Crypt::randomBytesBase64());
if (isset($old_session_id)) { if (isset($old_session_id)) {
$params = session_get_cookie_params(); $params = session_get_cookie_params();
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* Definition of Drupal\Component\PhpStorage\FileStorage. * Contains \Drupal\Component\PhpStorage\FileStorage.
*/ */
namespace Drupal\Component\PhpStorage; namespace Drupal\Component\PhpStorage;
...@@ -53,10 +53,82 @@ public function load($name) { ...@@ -53,10 +53,82 @@ public function load($name) {
*/ */
public function save($name, $code) { public function save($name, $code) {
$path = $this->getFullPath($name); $path = $this->getFullPath($name);
$this->ensureDirectory(dirname($path)); $directory = dirname($path);
if ($this->ensureDirectory($directory)) {
$htaccess_path = $directory . '/.htaccess';
if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) {
@chmod($htaccess_path, 0444);
}
}
return (bool) file_put_contents($path, $code); return (bool) file_put_contents($path, $code);
} }
/**
* Returns the standard .htaccess lines that Drupal writes to file directories.
*
* This code is located here so this component can be stand-alone, but it is
* also called by file_htaccess_lines().
*
* @param bool $private
* (Optional) Set to FALSE to return the .htaccess lines for an open and
* public directory. The default is TRUE, which returns the .htaccess lines
* for a private and protected directory.
*
* @return string
* The desired contents of the .htaccess file.
*/
public static function htaccessLines($private = TRUE) {
$lines = <<<EOF
# Turn off all options we don't need.
Options None
Options +FollowSymLinks
# Set the catch-all handler to prevent scripts from being executed.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
<Files *>
# Override the handler again if we're run later in the evaluation list.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
# If we know how to do it safely, disable the PHP engine entirely.
<IfModule mod_php5.c>
php_flag engine off
</IfModule>
EOF;
if ($private) {
$lines = "Deny from all\n\n" . $lines;
}
return $lines;
}
/**
* Ensures the directory exists, has the right permissions, and a .htaccess.
*
* For compatibility with open_basedir, the requested directory is created
* using a recursion logic that is based on the relative directory path/tree:
* It works from the end of the path recursively back towards the root
* directory, until an existing parent directory is found. From there, the
* subdirectories are created.
*
* @param string $directory
* The directory path.
* @param int $mode
* The mode, permissions, the directory should have.
*
* @return bool
* TRUE if the directory exists or has been created, FALSE otherwise.
*/
protected function ensureDirectory($directory, $mode = 0777) {
if ($this->createDirectory($directory, $mode)) {
$htaccess_path = $directory . '/.htaccess';
if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) {
@chmod($htaccess_path, 0444);
}
}
}
/** /**
* Ensures the requested directory exists and has the right permissions. * Ensures the requested directory exists and has the right permissions.
* *
...@@ -76,7 +148,7 @@ public function save($name, $code) { ...@@ -76,7 +148,7 @@ public function save($name, $code) {
* @return bool * @return bool
* TRUE if the directory exists or has been created, FALSE otherwise. * TRUE if the directory exists or has been created, FALSE otherwise.
*/ */
protected function ensureDirectory($directory, $mode = 0777, $is_backwards_recursive = FALSE) { protected function createDirectory($directory, $mode = 0777, $is_backwards_recursive = FALSE) {
// If the directory exists already, there's nothing to do. // If the directory exists already, there's nothing to do.
if (is_dir($directory)) { if (is_dir($directory)) {
return TRUE; return TRUE;
...@@ -97,7 +169,7 @@ protected function ensureDirectory($directory, $mode = 0777, $is_backwards_recur ...@@ -97,7 +169,7 @@ protected function ensureDirectory($directory, $mode = 0777, $is_backwards_recur
// until an existing directory is hit, and from there, recursively create // until an existing directory is hit, and from there, recursively create
// the sub-directories. Only if that recursion succeeds, create the final, // the sub-directories. Only if that recursion succeeds, create the final,
// originally requested subdirectory. // originally requested subdirectory.
return $this->ensureDirectory($parent, $mode, TRUE) && mkdir($directory) && chmod($directory, $mode); return $this->createDirectory($parent, $mode, TRUE) && mkdir($directory) && chmod($directory, $mode);
} }
/** /**
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* Definition of Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage. * Contains \Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage.
*/ */
namespace Drupal\Component\PhpStorage; namespace Drupal\Component\PhpStorage;
...@@ -39,13 +39,6 @@ ...@@ -39,13 +39,6 @@
*/ */
class MTimeProtectedFastFileStorage extends FileStorage { class MTimeProtectedFastFileStorage extends FileStorage {
/**
* The .htaccess code to make a directory private.
*
* Disabling Options Indexes is particularly important.
*/
const HTACCESS="SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks";
/** /**
* The secret used in the HMAC. * The secret used in the HMAC.
* *
...@@ -74,10 +67,6 @@ public function __construct(array $configuration) { ...@@ -74,10 +67,6 @@ public function __construct(array $configuration) {
*/ */
public function save($name, $data) { public function save($name, $data) {
$this->ensureDirectory($this->directory); $this->ensureDirectory($this->directory);
$htaccess_path = $this->directory . '/.htaccess';
if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, self::HTACCESS)) {
@chmod($htaccess_path, 0444);
}
// Write the file out to a temporary location. Prepend with a '.' to keep it // Write the file out to a temporary location. Prepend with a '.' to keep it
// hidden from listings and web servers. // hidden from listings and web servers.
......
...@@ -27,30 +27,29 @@ class Crypt { ...@@ -27,30 +27,29 @@ class Crypt {
* A randomly generated string. * A randomly generated string.
*/ */
public static function randomBytes($count) { public static function randomBytes($count) {
static $random_state, $bytes; // $random_state does not use drupal_static as it stores random bytes.
// Initialize on the first call. The contents of $_SERVER includes a mix of static $random_state, $bytes, $has_openssl;
// user-specific and system information that varies a little with each page.
// Further initialize with the somewhat random PHP process ID. $missing_bytes = $count - strlen($bytes);
if (!isset($random_state)) {
$random_state = print_r($_SERVER, TRUE) . getmypid(); if ($missing_bytes > 0) {
$bytes = ''; // openssl_random_pseudo_bytes() will find entropy in a system-dependent
// way.
if (function_exists('openssl_random_pseudo_bytes')) {
$bytes .= openssl_random_pseudo_bytes($missing_bytes);
} }
if (strlen($bytes) < $count) {
// /dev/urandom is available on many *nix systems and is considered the // Else, read directly from /dev/urandom, which is available on many *nix
// best commonly available pseudo-random source. // systems and is considered cryptographically secure.
if ($fh = @fopen('/dev/urandom', 'rb')) { elseif ($fh = @fopen('/dev/urandom', 'rb')) {
// PHP only performs buffered reads, so in reality it will always read // PHP only performs buffered reads, so in reality it will always read
// at least 4096 bytes. Thus, it costs nothing extra to read and store // at least 4096 bytes. Thus, it costs nothing extra to read and store
// that much so as to speed any additional invocations. // that much so as to speed any additional invocations.
$bytes .= fread($fh, max(4096, $count)); $bytes .= fread($fh, max(4096, $missing_bytes));
fclose($fh); fclose($fh);
} }
// openssl_random_pseudo_bytes() will find entropy in a system-dependent
// way. // If we couldn't get enough entropy, this simple hash-based PRNG will
elseif (function_exists('openssl_random_pseudo_bytes')) {
$bytes .= openssl_random_pseudo_bytes($count - strlen($bytes));
}
// If /dev/urandom is not available or returns no bytes, this loop will
// generate a good set of pseudo-random bytes on any system. // generate a good set of pseudo-random bytes on any system.
// Note that it may be important that our $random_state is passed // Note that it may be important that our $random_state is passed
// through hash() prior to being rolled into $output, that the two hash() // through hash() prior to being rolled into $output, that the two hash()
...@@ -58,9 +57,23 @@ public static function randomBytes($count) { ...@@ -58,9 +57,23 @@ public static function randomBytes($count) {
// the microtime() - is prepended rather than appended. This is to avoid // the microtime() - is prepended rather than appended. This is to avoid
// directly leaking $random_state via the $output stream, which could // directly leaking $random_state via the $output stream, which could
// allow for trivial prediction of further "random" numbers. // allow for trivial prediction of further "random" numbers.
while (strlen($bytes) < $count) { if (strlen($bytes) < $count) {
// Initialize on the first call. The contents of $_SERVER includes a mix
// of user-specific and system information that varies a little with
// each page.
if (!isset($random_state)) {
$random_state = print_r($_SERVER, TRUE);
if (function_exists('getmypid')) {
// Further initialize with the somewhat random PHP process ID.
$random_state .= getmypid();
}
$bytes = '';
}
do {
$random_state = hash('sha256', microtime() . mt_rand() . $random_state); $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
$bytes .= hash('sha256', mt_rand() . $random_state, TRUE); $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
} while (strlen($bytes) < $count);
} }
} }
$output = substr($bytes, 0, $count); $output = substr($bytes, 0, $count);
...@@ -111,20 +124,18 @@ public static function hashBase64($data) { ...@@ -111,20 +124,18 @@ public static function hashBase64($data) {
} }
/** /**
* Generates a random, base-64 encoded, URL-safe, sha-256 hashed string. * Returns a URL-safe, base64 encoded string of highly randomized bytes.
* *
* @param int $count * @param $byte_count
* The number of characters (bytes) of the string to be hashed. * The number of random bytes to fetch and base64 encode.
* *
* @return string * @return string
* A base-64 encoded sha-256 hash, with + replaced with -, / with _ and * The base64 encoded result will have a length of up to 4 * $byte_count.
* any = padding characters removed.
* *
* @see \Drupal\Component\Utility\Crypt::randomBytes() * @see \Drupal\Component\Utility\Crypt::randomBytes()
* @see \Drupal\Component\Utility\Crypt::hashBase64()
*/ */
public static function randomStringHashed($count) { public static function randomBytesBase64($count = 32) {
return static::hashBase64(static::randomBytes($count)); return strtr(base64_encode(static::randomBytes($count)), array('+' => '-', '/' => '_', '=' => ''));
} }
} }
...@@ -329,7 +329,7 @@ public function rebuildForm($form_id, &$form_state, $old_form = NULL) { ...@@ -329,7 +329,7 @@ public function rebuildForm($form_id, &$form_state, $old_form = NULL) {
$form['#build_id'] = $old_form['#build_id']; $form['#build_id'] = $old_form['#build_id'];
} }
else { else {
$form['#build_id'] = 'form-' . Crypt::hashBase64(uniqid(mt_rand(), TRUE) . mt_rand()); $form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
} }
// #action defaults to request_uri(), but in case of Ajax and other partial // #action defaults to request_uri(), but in case of Ajax and other partial
...@@ -672,7 +672,7 @@ public function prepareForm($form_id, &$form, &$form_state) { ...@@ -672,7 +672,7 @@ public function prepareForm($form_id, &$form, &$form_state) {
// @see self::buildForm() // @see self::buildForm()
// @see self::rebuildForm() // @see self::rebuildForm()
if (!isset($form['#build_id'])) { if (!isset($form['#build_id'])) {
$form['#build_id'] = 'form-' . Crypt::hashBase64(uniqid(mt_rand(), TRUE) . mt_rand()); $form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
} }
$form['form_build_id'] = array( $form['form_build_id'] = array(
'#type' => 'hidden', '#type' => 'hidden',
......
...@@ -64,7 +64,7 @@ public function set($key) { ...@@ -64,7 +64,7 @@ public function set($key) {
* The private key. * The private key.
*/ */
protected function create() { protected function create() {
return Crypt::randomStringHashed(55); return Crypt::randomBytesBase64(55);
} }
} }
...@@ -99,7 +99,7 @@ function testFileCheckDirectoryHandling() { ...@@ -99,7 +99,7 @@ function testFileCheckDirectoryHandling() {
$this->assertTrue(is_file(file_default_scheme() . '://.htaccess'), 'Successfully re-created the .htaccess file in the files directory.', 'File'); $this->assertTrue(is_file(file_default_scheme() . '://.htaccess'), 'Successfully re-created the .htaccess file in the files directory.', 'File');
// Verify contents of .htaccess file. // Verify contents of .htaccess file.
$file = file_get_contents(file_default_scheme() . '://.htaccess'); $file = file_get_contents(file_default_scheme() . '://.htaccess');
$this->assertEqual($file, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks", 'The .htaccess file contains the proper content.', 'File'); $this->assertEqual($file, file_htaccess_lines(FALSE), 'The .htaccess file contains the proper content.', 'File');
} }
/** /**
......
...@@ -43,7 +43,10 @@ function testHtaccessSave() { ...@@ -43,7 +43,10 @@ function testHtaccessSave() {
mkdir($public, 0777, TRUE); mkdir($public, 0777, TRUE);
$this->assertTrue(file_save_htaccess($public, FALSE)); $this->assertTrue(file_save_htaccess($public, FALSE));
$content = file_get_contents($public . '/.htaccess'); $content = file_get_contents($public . '/.htaccess');
$this->assertIdentical($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks"); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE);
$this->assertTrue(strpos($content, "Options None") !== FALSE);
$this->assertTrue(strpos($content, "Options +FollowSymLinks") !== FALSE);
$this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE);
$this->assertFilePermissions($public . '/.htaccess', 0444); $this->assertFilePermissions($public . '/.htaccess', 0444);
$this->assertTrue(file_save_htaccess($public, FALSE)); $this->assertTrue(file_save_htaccess($public, FALSE));
...@@ -52,7 +55,11 @@ function testHtaccessSave() { ...@@ -52,7 +55,11 @@ function testHtaccessSave() {
mkdir($private, 0777, TRUE); mkdir($private, 0777, TRUE);
$this->assertTrue(file_save_htaccess($private)); $this->assertTrue(file_save_htaccess($private));
$content = file_get_contents($private . '/.htaccess'); $content = file_get_contents($private . '/.htaccess');
$this->assertIdentical($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks"); $this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE);
$this->assertTrue(strpos($content, "Deny from all") !== FALSE);
$this->assertTrue(strpos($content, "Options None") !== FALSE);
$this->assertTrue(strpos($content, "Options +FollowSymLinks") !== FALSE);
$this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE);
$this->assertFilePermissions($private . '/.htaccess', 0444); $this->assertFilePermissions($private . '/.htaccess', 0444);
$this->assertTrue(file_save_htaccess($private)); $this->assertTrue(file_save_htaccess($private));
...@@ -61,7 +68,11 @@ function testHtaccessSave() { ...@@ -61,7 +68,11 @@ function testHtaccessSave() {
mkdir($stream, 0777, TRUE); mkdir($stream, 0777, TRUE);
$this->assertTrue(file_save_htaccess($stream)); $this->assertTrue(file_save_htaccess($stream));
$content = file_get_contents($stream . '/.htaccess'); $content = file_get_contents($stream . '/.htaccess');
$this->assertIdentical($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nDeny from all\nOptions None\nOptions +FollowSymLinks"); $this->assertTrue(strpos($content,"SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006") !== FALSE);
$this->assertTrue(strpos($content,"Deny from all") !== FALSE);
$this->assertTrue(strpos($content,"Options None") !== FALSE);
$this->assertTrue(strpos($content,"Options +FollowSymLinks") !== FALSE);
$this->assertTrue(strpos($content, "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003") !== FALSE);
$this->assertFilePermissions($stream . '/.htaccess', 0444); $this->assertFilePermissions($stream . '/.htaccess', 0444);
$this->assertTrue(file_save_htaccess($stream)); $this->assertTrue(file_save_htaccess($stream));
......
...@@ -264,6 +264,39 @@ function system_requirements($phase) { ...@@ -264,6 +264,39 @@ function system_requirements($phase) {
$requirements['settings.php']['title'] = t('Configuration files'); $requirements['settings.php']['title'] = t('Configuration files');
} }
// Test the contents of the .htaccess files.
if ($phase == 'runtime') {
// Try to write the .htaccess files first, to prevent false alarms in case
// (for example) the /tmp directory was wiped.
file_ensure_htaccess();
$htaccess_files['public://.htaccess'] = array(
'title' => t('Public files directory'),
'directory' => drupal_realpath('public://'),
);
if (\Drupal::config('system.file')->get('path.private')) {
$htaccess_files['private://.htaccess'] = array(
'title' => t('Private files directory'),
'directory' => drupal_realpath('private://'),
);
}
$htaccess_files['temporary://.htaccess'] = array(
'title' => t('Temporary files directory'),
'directory' => drupal_realpath('temporary://'),
);