Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,105 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\FileReadOnlyStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Reads code as regular PHP files, but won't write them.
*/
class FileReadOnlyStorage implements PhpStorageInterface {
/**
* The directory where the files should be stored.
*
* @var string
*/
protected $directory;
/**
* Constructs this FileStorage object.
*
* @param $configuration
* An associative array, containing at least two keys (the rest are ignored):
* - directory: The directory where the files should be stored.
* - bin: The storage bin. Multiple storage objects can be instantiated with
* the same configuration, but for different bins.
*/
public function __construct(array $configuration) {
$this->directory = $configuration['directory'] . '/' . $configuration['bin'];
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::exists().
*/
public function exists($name) {
return file_exists($this->getFullPath($name));
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::load().
*/
public function load($name) {
// The FALSE returned on failure is enough for the caller to handle this,
// we do not want a warning too.
return (@include_once $this->getFullPath($name)) !== FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save().
*/
public function save($name, $code) {
return FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::delete().
*/
public function delete($name) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getFullPath($name) {
return $this->directory . '/' . $name;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::writeable().
*/
function writeable() {
return FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::deleteAll().
*/
public function deleteAll() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function listAll() {
$names = array();
if (file_exists($this->directory)) {
foreach (new \DirectoryIterator($this->directory) as $fileinfo) {
if (!$fileinfo->isDot()) {
$name = $fileinfo->getFilename();
if ($name != '.htaccess') {
$names[] = $name;
}
}
}
}
return $names;
}
}

View file

@ -0,0 +1,269 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\FileStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores the code as regular PHP files.
*/
class FileStorage implements PhpStorageInterface {
/**
* The directory where the files should be stored.
*
* @var string
*/
protected $directory;
/**
* Constructs this FileStorage object.
*
* @param array $configuration
* An associative array, containing at least these two keys:
* - directory: The directory where the files should be stored.
* - bin: The storage bin. Multiple storage objects can be instantiated with
* the same configuration, but for different bins..
*/
public function __construct(array $configuration) {
$this->directory = $configuration['directory'] . '/' . $configuration['bin'];
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::exists().
*/
public function exists($name) {
return file_exists($this->getFullPath($name));
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::load().
*/
public function load($name) {
// The FALSE returned on failure is enough for the caller to handle this,
// we do not want a warning too.
return (@include_once $this->getFullPath($name)) !== FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save().
*/
public function save($name, $code) {
$path = $this->getFullPath($name);
$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);
}
/**
* Returns the standard .htaccess lines that Drupal writes to file directories.
*
* @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.
*
* @see file_create_htaccess()
*/
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 = <<<EOF
# Deny all requests from Apache 2.4+.
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
# Deny all requests from Apache 2.0-2.2.
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
$lines
EOF;
}
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.
*
* 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.
* @param bool $is_backwards_recursive
* Internal use only.
*
* @return bool
* TRUE if the directory exists or has been created, FALSE otherwise.
*/
protected function createDirectory($directory, $mode = 0777, $is_backwards_recursive = FALSE) {
// If the directory exists already, there's nothing to do.
if (is_dir($directory)) {
return TRUE;
}
// Otherwise, try to create the directory and ensure to set its permissions,
// because mkdir() obeys the umask of the current process.
if (is_dir($parent = dirname($directory))) {
// If the parent directory exists, then the backwards recursion must end,
// regardless of whether the subdirectory could be created.
if ($status = mkdir($directory)) {
// Only try to chmod() if the subdirectory could be created.
$status = chmod($directory, $mode);
}
return $is_backwards_recursive ? TRUE : $status;
}
// If the parent directory and the requested directory does not exist and
// could not be created above, walk the requested directory path back up
// until an existing directory is hit, and from there, recursively create
// the sub-directories. Only if that recursion succeeds, create the final,
// originally requested subdirectory.
return $this->createDirectory($parent, $mode, TRUE) && mkdir($directory) && chmod($directory, $mode);
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::delete().
*/
public function delete($name) {
$path = $this->getFullPath($name);
if (file_exists($path)) {
return $this->unlink($path);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getFullPath($name) {
return $this->directory . '/' . $name;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::writeable().
*/
public function writeable() {
return TRUE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::deleteAll().
*/
public function deleteAll() {
return $this->unlink($this->directory);
}
/**
* Deletes files and/or directories in the specified path.
*
* If the specified path is a directory the method will
* call itself recursively to process the contents. Once the contents have
* been removed the directory will also be removed.
*
* @param string $path
* A string containing either a file or directory path.
*
* @return boolean
* TRUE for success or if path does not exist, FALSE in the event of an
* error.
*/
protected function unlink($path) {
if (file_exists($path)) {
if (is_dir($path)) {
// Ensure the folder is writable.
@chmod($path, 0777);
foreach (new \DirectoryIterator($path) as $fileinfo) {
if (!$fileinfo->isDot()) {
$this->unlink($fileinfo->getPathName());
}
}
return @rmdir($path);
}
// Windows needs the file to be writable.
@chmod($path, 0700);
return @unlink($path);
}
// If there's nothing to delete return TRUE anyway.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function listAll() {
$names = array();
if (file_exists($this->directory)) {
foreach (new \DirectoryIterator($this->directory) as $fileinfo) {
if (!$fileinfo->isDot()) {
$name = $fileinfo->getFilename();
if ($name != '.htaccess') {
$names[] = $name;
}
}
}
}
return $names;
}
}

View file

@ -0,0 +1,211 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores PHP code in files with securely hashed names.
*
* The goal of this class is to ensure that if a PHP file is replaced with
* an untrusted one, it does not get loaded. Since mtime granularity is 1
* second, we cannot prevent an attack that happens within one second of the
* initial save(). However, it is very unlikely for an attacker exploiting an
* upload or file write vulnerability to also know when a legitimate file is
* being saved, discover its hash, undo its file permissions, and override the
* file with an upload all within a single second. Being able to accomplish
* that would indicate a site very likely vulnerable to many other attack
* vectors.
*
* Each file is stored in its own unique containing directory. The hash is based
* on the virtual file name, the containing directory's mtime, and a
* cryptographically hard to guess secret string. Thus, even if the hashed file
* name is discovered and replaced by an untrusted file (e.g., via a
* move_uploaded_file() invocation by a script that performs insufficient
* validation), the directory's mtime gets updated in the process, invalidating
* the hash and preventing the untrusted file from getting loaded.
*
* This class does not protect against overwriting a file in-place (e.g. a
* malicious module that does a file_put_contents()) since this will not change
* the mtime of the directory. MTimeProtectedFileStorage protects against this
* at the cost of an additional system call for every load() and exists().
*
* The containing directory is created with the same name as the virtual file
* name (slashes removed) to assist with debugging, since the file itself is
* stored with a name that's meaningless to humans.
*/
class MTimeProtectedFastFileStorage extends FileStorage {
/**
* The secret used in the HMAC.
*
* @var string
*/
protected $secret;
/**
* Constructs this MTimeProtectedFastFileStorage object.
*
* @param array $configuration
* An associated array, containing at least these keys (the rest are
* ignored):
* - directory: The directory where the files should be stored.
* - secret: A cryptographically hard to guess secret string.
* -bin. The storage bin. Multiple storage objects can be instantiated with
* the same configuration, but for different bins.
*/
public function __construct(array $configuration) {
parent::__construct($configuration);
$this->secret = $configuration['secret'];
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save().
*/
public function save($name, $data) {
$this->ensureDirectory($this->directory);
// Write the file out to a temporary location. Prepend with a '.' to keep it
// hidden from listings and web servers.
$temporary_path = $this->tempnam($this->directory, '.');
if (!$temporary_path || !@file_put_contents($temporary_path, $data)) {
return FALSE;
}
// The file will not be chmod() in the future so this is the final
// permission.
chmod($temporary_path, 0444);
// Prepare a directory dedicated for just this file. Ensure it has a current
// mtime so that when the file (hashed on that mtime) is moved into it, the
// mtime remains the same (unless the clock ticks to the next second during
// the rename, in which case we'll try again).
$directory = $this->getContainingDirectoryFullPath($name);
if (file_exists($directory)) {
$this->unlink($directory);
}
$this->ensureDirectory($directory);
// Move the file to its final place. The mtime of a directory is the time of
// the last file create or delete in the directory. So the moving will
// update the directory mtime. However, this update will very likely not
// show up, because it has a coarse, one second granularity and typical
// moves takes significantly less than that. In the unlucky case the clock
// ticks during the move, we need to keep trying until the mtime we hashed
// on and the updated mtime match.
$previous_mtime = 0;
$i = 0;
while (($mtime = $this->getUncachedMTime($directory)) && ($mtime != $previous_mtime)) {
$previous_mtime = $mtime;
// Reset the file back in the temporary location if this is not the first
// iteration.
if ($i > 0) {
$this->unlink($temporary_path);
$temporary_path = $this->tempnam($this->directory, '.');
rename($full_path, $temporary_path);
// Make sure to not loop infinitely on a hopelessly slow filesystem.
if ($i > 10) {
$this->unlink($temporary_path);
return FALSE;
}
}
$full_path = $this->getFullPath($name, $directory, $mtime);
rename($temporary_path, $full_path);
$i++;
}
return TRUE;
}
/**
* Gets the full path where the file is or should be stored.
*
* This function creates a file path that includes a unique containing
* directory for the file and a file name that is a hash of the virtual file
* name, a cryptographic secret, and the containing directory mtime. If the
* file is overridden by an insecure upload script, the directory mtime gets
* modified, invalidating the file, thus protecting against untrusted code
* getting executed.
*
* @param string $name
* The virtual file name. Can be a relative path.
* @param string $directory
* (optional) The directory containing the file. If not passed, this is
* retrieved by calling getContainingDirectoryFullPath().
* @param int $directory_mtime
* (optional) The mtime of $directory. Can be passed to avoid an extra
* filesystem call when the mtime of the directory is already known.
*
* @return string
* The full path where the file is or should be stored.
*/
public function getFullPath($name, &$directory = NULL, &$directory_mtime = NULL) {
if (!isset($directory)) {
$directory = $this->getContainingDirectoryFullPath($name);
}
if (!isset($directory_mtime)) {
$directory_mtime = file_exists($directory) ? filemtime($directory) : 0;
}
return $directory . '/' . hash_hmac('sha256', $name, $this->secret . $directory_mtime) . '.php';
}
/**
* {@inheritdoc}
*/
public function delete($name) {
$path = $this->getContainingDirectoryFullPath($name);
if (file_exists($path)) {
return $this->unlink($path);
}
return FALSE;
}
/**
* Gets the full path of the containing directory where the file is or should
* be stored.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return string
* The full path of the containing directory where the file is or should be
* stored.
*/
protected function getContainingDirectoryFullPath($name) {
// Remove the .php file extension from the directory name.
// Within a single directory, a subdirectory cannot have the same name as a
// file. Thus, when switching between MTimeProtectedFastFileStorage and
// FileStorage, the subdirectory or the file cannot be created in case the
// other file type exists already.
if (substr($name, -4) === '.php') {
$name = substr($name, 0, -4);
}
return $this->directory . '/' . str_replace('/', '#', $name);
}
/**
* Clears PHP's stat cache and returns the directory's mtime.
*/
protected function getUncachedMTime($directory) {
clearstatcache(TRUE, $directory);
return filemtime($directory);
}
/**
* A brute force tempnam implementation supporting streams.
*
* @param $directory
* The directory where the temporary filename will be created.
* @param $prefix
* The prefix of the generated temporary filename.
* @return string
* Returns the new temporary filename (with path), or FALSE on failure.
*/
protected function tempnam($directory, $prefix) {
do {
$path = $directory . '/' . $prefix . substr(str_shuffle(hash('sha256', microtime())), 0, 10);
} while (file_exists($path));
return $path;
}
}

View file

@ -0,0 +1,76 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\MTimeProtectedFileStorage.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores PHP code in files with securely hashed names.
*
* The goal of this class is to ensure that if a PHP file is replaced with
* an untrusted one, it does not get loaded. Since mtime granularity is 1
* second, we cannot prevent an attack that happens within one second of the
* initial save(). However, it is very unlikely for an attacker exploiting an
* upload or file write vulnerability to also know when a legitimate file is
* being saved, discover its hash, undo its file permissions, and override the
* file with an upload all within a single second. Being able to accomplish
* that would indicate a site very likely vulnerable to many other attack
* vectors.
*
* Each file is stored in its own unique containing directory. The hash is
* based on the virtual file name, the containing directory's mtime, and a
* cryptographically hard to guess secret string. Thus, even if the hashed file
* name is discovered and replaced by an untrusted file (e.g., via a
* move_uploaded_file() invocation by a script that performs insufficient
* validation), the directory's mtime gets updated in the process, invalidating
* the hash and preventing the untrusted file from getting loaded. Also, the
* file mtime will be checked providing security against overwriting in-place,
* at the cost of an additional system call for every load() and exists().
*
* The containing directory is created with the same name as the virtual file
* name (slashes replaced with hashmarks) to assist with debugging, since the
* file itself is stored with a name that's meaningless to humans.
*/
class MTimeProtectedFileStorage extends MTimeProtectedFastFileStorage {
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::load().
*/
public function load($name) {
if (($filename = $this->checkFile($name)) !== FALSE) {
// Inline parent::load() to avoid an expensive getFullPath() call.
return (@include_once $filename) !== FALSE;
}
return FALSE;
}
/**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::exists().
*/
public function exists($name) {
return $this->checkFile($name) !== FALSE;
}
/**
* Determines whether a protected file exists and sets the filename too.
*
* @param string $name
* The virtual file name. Can be a relative path.
* return string
* The full path where the file is if it is valid, FALSE otherwise.
*/
protected function checkFile($name) {
$filename = $this->getFullPath($name, $directory, $directory_mtime);
return file_exists($filename) && filemtime($filename) <= $directory_mtime ? $filename : FALSE;
}
/**
* {@inheritdoc}
*/
public function getPath($name) {
return $this->checkFile($name);
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* @file
* Contains \Drupal\Component\PhpStorage\PhpStorageInterface.
*/
namespace Drupal\Component\PhpStorage;
/**
* Stores and loads PHP code.
*
* Each interface function takes $name as a parameter. This is a virtual file
* name: for example, 'foo.php' or 'some/relative/path/to/foo.php'. The
* storage implementation may store these as files within the local file system,
* use a remote stream, combine multiple virtual files into an archive, store
* them in database records, or use some other storage technique.
*/
interface PhpStorageInterface {
/**
* Checks whether the PHP code exists in storage.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return bool
* TRUE if the virtual file exists, FALSE otherwise.
*/
public function exists($name);
/**
* Loads PHP code from storage.
*
* Depending on storage implementation, exists() checks can be expensive, so
* this function may be called for a file that doesn't exist, and that should
* not result in errors. This function does not return anything, so it is
* up to the caller to determine if any code was loaded (for example, check
* class_exists() or function_exists() for what was expected in the code).
*
* @param string $name
* The virtual file name. Can be a relative path.
*/
public function load($name);
/**
* Saves PHP code to storage.
*
* @param string $name
* The virtual file name. Can be a relative path.
* @param string $code
* The PHP code to be saved.
*
* @return bool
* TRUE if the save succeeded, FALSE if it failed.
*/
public function save($name, $code);
/**
* Whether this is a writeable storage.
*
* @return bool
*/
public function writeable();
/**
* Deletes PHP code from storage.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return bool
* TRUE if the delete succeeded, FALSE if it failed.
*/
public function delete($name);
/**
* Removes all files in this bin.
*/
public function deleteAll();
/**
* Gets the full file path.
*
* @param string $name
* The virtual file name. Can be a relative path.
*
* @return string|FALSE
* The full file path for the provided name. Return FALSE if the
* implementation needs to prevent access to the file.
*/
public function getFullPath($name);
/**
* Lists all the files in the storage.
*
* @return array
* Array of filenames.
*/
public function listAll();
}