Update to Drupal 8.0.4. For more information, see https://www.drupal.org/drupal-8.0.4
This commit is contained in:
parent
9db4fae9a7
commit
315484f62f
|
@ -1,3 +1,7 @@
|
||||||
|
Drupal 8.0.4, 2016-02-24
|
||||||
|
------------------------
|
||||||
|
- Fixed security issues (multiple vulnerabilities). See SA-CORE-2016-001.
|
||||||
|
|
||||||
Drupal 8.0.0, 2015-11-19
|
Drupal 8.0.0, 2015-11-19
|
||||||
------------------------
|
------------------------
|
||||||
- Significantly improved the front end:
|
- Significantly improved the front end:
|
||||||
|
|
|
@ -81,7 +81,7 @@ class Drupal {
|
||||||
/**
|
/**
|
||||||
* The current system version.
|
* The current system version.
|
||||||
*/
|
*/
|
||||||
const VERSION = '8.0.3';
|
const VERSION = '8.0.4';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core API compatibility.
|
* Core API compatibility.
|
||||||
|
|
|
@ -216,12 +216,19 @@ class UrlHelper {
|
||||||
*/
|
*/
|
||||||
public static function isExternal($path) {
|
public static function isExternal($path) {
|
||||||
$colonpos = strpos($path, ':');
|
$colonpos = strpos($path, ':');
|
||||||
// Avoid calling drupal_strip_dangerous_protocols() if there is any slash
|
// Some browsers treat \ as / so normalize to forward slashes.
|
||||||
// (/), hash (#) or question_mark (?) before the colon (:) occurrence - if
|
$path = str_replace('\\', '/', $path);
|
||||||
// any - as this would clearly mean it is not a URL. If the path starts with
|
// If the path starts with 2 slashes then it is always considered an
|
||||||
// 2 slashes then it is always considered an external URL without an
|
// external URL without an explicit protocol part.
|
||||||
// explicit protocol part.
|
|
||||||
return (strpos($path, '//') === 0)
|
return (strpos($path, '//') === 0)
|
||||||
|
// Leading control characters may be ignored or mishandled by browsers,
|
||||||
|
// so assume such a path may lead to an external location. The \p{C}
|
||||||
|
// character class matches all UTF-8 control, unassigned, and private
|
||||||
|
// characters.
|
||||||
|
|| (preg_match('/^\p{C}/u', $path) !== 0)
|
||||||
|
// Avoid calling static::stripDangerousProtocols() if there is any slash
|
||||||
|
// (/), hash (#) or question_mark (?) before the colon (:) occurrence -
|
||||||
|
// if any - as this would clearly mean it is not a URL.
|
||||||
|| ($colonpos !== FALSE
|
|| ($colonpos !== FALSE
|
||||||
&& !preg_match('![/?#]!', substr($path, 0, $colonpos))
|
&& !preg_match('![/?#]!', substr($path, 0, $colonpos))
|
||||||
&& static::stripDangerousProtocols($path) == $path);
|
&& static::stripDangerousProtocols($path) == $path);
|
||||||
|
|
|
@ -64,6 +64,7 @@ class ManagedFile extends FormElement {
|
||||||
foreach ($fids as $key => $fid) {
|
foreach ($fids as $key => $fid) {
|
||||||
$fids[$key] = (int) $fid;
|
$fids[$key] = (int) $fid;
|
||||||
}
|
}
|
||||||
|
$force_default = FALSE;
|
||||||
|
|
||||||
// Process any input and save new uploads.
|
// Process any input and save new uploads.
|
||||||
if ($input !== FALSE) {
|
if ($input !== FALSE) {
|
||||||
|
@ -95,15 +96,26 @@ class ManagedFile extends FormElement {
|
||||||
$fids = [];
|
$fids = [];
|
||||||
foreach ($input['fids'] as $fid) {
|
foreach ($input['fids'] as $fid) {
|
||||||
if ($file = File::load($fid)) {
|
if ($file = File::load($fid)) {
|
||||||
$fids[] = $file->id();
|
// Temporary files that belong to other users should never be
|
||||||
|
// allowed. Since file ownership can't be determined for anonymous
|
||||||
|
// users, they are not allowed to reuse temporary files at all.
|
||||||
|
if ($file->isTemporary() && (\Drupal::currentUser()->isAnonymous() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
|
||||||
|
$force_default = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// If all checks pass, allow the files to be changed.
|
||||||
|
else {
|
||||||
|
$fids[] = $file->id();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no input, set the default value.
|
// If there is no input or if the default value was requested above, use the
|
||||||
else {
|
// default value.
|
||||||
|
if ($input === FALSE || $force_default) {
|
||||||
if ($element['#extended']) {
|
if ($element['#extended']) {
|
||||||
$default_fids = isset($element['#default_value']['fids']) ? $element['#default_value']['fids'] : [];
|
$default_fids = isset($element['#default_value']['fids']) ? $element['#default_value']['fids'] : [];
|
||||||
$return = isset($element['#default_value']) ? $element['#default_value'] : ['fids' => []];
|
$return = isset($element['#default_value']) ? $element['#default_value'] : ['fids' => []];
|
||||||
|
|
|
@ -10,11 +10,14 @@ namespace Drupal\file\Tests;
|
||||||
use Drupal\comment\Entity\Comment;
|
use Drupal\comment\Entity\Comment;
|
||||||
use Drupal\comment\Tests\CommentTestTrait;
|
use Drupal\comment\Tests\CommentTestTrait;
|
||||||
use Drupal\Component\Utility\Unicode;
|
use Drupal\Component\Utility\Unicode;
|
||||||
|
use Drupal\Core\Url;
|
||||||
use Drupal\field\Entity\FieldConfig;
|
use Drupal\field\Entity\FieldConfig;
|
||||||
use Drupal\field\Entity\FieldStorageConfig;
|
use Drupal\field\Entity\FieldStorageConfig;
|
||||||
use Drupal\field_ui\Tests\FieldUiTestTrait;
|
use Drupal\field_ui\Tests\FieldUiTestTrait;
|
||||||
use Drupal\user\RoleInterface;
|
use Drupal\user\RoleInterface;
|
||||||
use Drupal\file\Entity\File;
|
use Drupal\file\Entity\File;
|
||||||
|
use Drupal\user\Entity\User;
|
||||||
|
use Drupal\user\UserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the file field widget, single and multi-valued, with and without AJAX,
|
* Tests the file field widget, single and multi-valued, with and without AJAX,
|
||||||
|
@ -42,6 +45,36 @@ class FileFieldWidgetTest extends FileFieldTestBase {
|
||||||
*/
|
*/
|
||||||
public static $modules = array('comment', 'block');
|
public static $modules = array('comment', 'block');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary file, for a specific user.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* A string containing the contents of the file.
|
||||||
|
* @param \Drupal\user\UserInterface $user
|
||||||
|
* The user of the file owner.
|
||||||
|
*
|
||||||
|
* @return \Drupal\file\FileInterface
|
||||||
|
* A file object, or FALSE on error.
|
||||||
|
*/
|
||||||
|
protected function createTemporaryFile($data, UserInterface $user = NULL) {
|
||||||
|
$file = file_save_data($data, NULL, NULL);
|
||||||
|
|
||||||
|
if ($file) {
|
||||||
|
if ($user) {
|
||||||
|
$file->setOwner($user);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$file->setOwner($this->adminUser);
|
||||||
|
}
|
||||||
|
// Change the file status to be temporary.
|
||||||
|
$file->setTemporary();
|
||||||
|
// Save the changes.
|
||||||
|
$file->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests upload and remove buttons for a single-valued File field.
|
* Tests upload and remove buttons for a single-valued File field.
|
||||||
*/
|
*/
|
||||||
|
@ -449,4 +482,113 @@ class FileFieldWidgetTest extends FileFieldTestBase {
|
||||||
$this->assertIdentical(count($elements), 1);
|
$this->assertIdentical(count($elements), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests exploiting the temporary file removal of another user using fid.
|
||||||
|
*/
|
||||||
|
public function testTemporaryFileRemovalExploit() {
|
||||||
|
// Create a victim user.
|
||||||
|
$victim_user = $this->drupalCreateUser();
|
||||||
|
|
||||||
|
// Create an attacker user.
|
||||||
|
$attacker_user = $this->drupalCreateUser(array(
|
||||||
|
'access content',
|
||||||
|
'create article content',
|
||||||
|
'edit any article content',
|
||||||
|
));
|
||||||
|
|
||||||
|
// Log in as the attacker user.
|
||||||
|
$this->drupalLogin($attacker_user);
|
||||||
|
|
||||||
|
// Perform tests using the newly created users.
|
||||||
|
$this->doTestTemporaryFileRemovalExploit($victim_user, $attacker_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests exploiting the temporary file removal for anonymous users using fid.
|
||||||
|
*/
|
||||||
|
public function testTemporaryFileRemovalExploitAnonymous() {
|
||||||
|
// Set up an anonymous victim user.
|
||||||
|
$victim_user = User::getAnonymousUser();
|
||||||
|
|
||||||
|
// Set up an anonymous attacker user.
|
||||||
|
$attacker_user = User::getAnonymousUser();
|
||||||
|
|
||||||
|
// Set up permissions for anonymous attacker user.
|
||||||
|
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array(
|
||||||
|
'access content' => TRUE,
|
||||||
|
'create article content' => TRUE,
|
||||||
|
'edit any article content' => TRUE,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Log out so as to be the anonymous attacker user.
|
||||||
|
$this->drupalLogout();
|
||||||
|
|
||||||
|
// Perform tests using the newly set up anonymous users.
|
||||||
|
$this->doTestTemporaryFileRemovalExploit($victim_user, $attacker_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for testing exploiting the temporary file removal using fid.
|
||||||
|
*
|
||||||
|
* @param \Drupal\user\UserInterface $victim_user
|
||||||
|
* The victim user.
|
||||||
|
* @param \Drupal\user\UserInterface $attacker_user
|
||||||
|
* The attacker user.
|
||||||
|
*/
|
||||||
|
protected function doTestTemporaryFileRemovalExploit(UserInterface $victim_user, UserInterface $attacker_user) {
|
||||||
|
$type_name = 'article';
|
||||||
|
$field_name = 'test_file_field';
|
||||||
|
$this->createFileField($field_name, 'node', $type_name);
|
||||||
|
|
||||||
|
$test_file = $this->getTestFile('text');
|
||||||
|
foreach (array('nojs', 'js') as $type) {
|
||||||
|
// Create a temporary file owned by the victim user. This will be as if
|
||||||
|
// they had uploaded the file, but not saved the node they were editing
|
||||||
|
// or creating.
|
||||||
|
$victim_tmp_file = $this->createTemporaryFile('some text', $victim_user);
|
||||||
|
$victim_tmp_file = File::load($victim_tmp_file->id());
|
||||||
|
$this->assertTrue($victim_tmp_file->isTemporary(), 'New file saved to disk is temporary.');
|
||||||
|
$this->assertFalse(empty($victim_tmp_file->id()), 'New file has an fid.');
|
||||||
|
$this->assertEqual($victim_user->id(), $victim_tmp_file->getOwnerId(), 'New file belongs to the victim.');
|
||||||
|
|
||||||
|
// Have attacker create a new node with a different uploaded file and
|
||||||
|
// ensure it got uploaded successfully.
|
||||||
|
$edit = [
|
||||||
|
'title[0][value]' => $type . '-title' ,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Attach a file to a node.
|
||||||
|
$edit['files[' . $field_name . '_0]'] = $this->container->get('file_system')->realpath($test_file->getFileUri());
|
||||||
|
$this->drupalPostForm(Url::fromRoute('node.add', array('node_type' => $type_name)), $edit, t('Save'));
|
||||||
|
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
|
||||||
|
|
||||||
|
/** @var \Drupal\file\FileInterface $node_file */
|
||||||
|
$node_file = File::load($node->{$field_name}->target_id);
|
||||||
|
$this->assertFileExists($node_file, 'A file was saved to disk on node creation');
|
||||||
|
$this->assertEqual($attacker_user->id(), $node_file->getOwnerId(), 'New file belongs to the attacker.');
|
||||||
|
|
||||||
|
// Ensure the file can be downloaded.
|
||||||
|
$this->drupalGet(file_create_url($node_file->getFileUri()));
|
||||||
|
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
|
||||||
|
|
||||||
|
// "Click" the remove button (emulating either a nojs or js submission).
|
||||||
|
// In this POST request, the attacker "guesses" the fid of the victim's
|
||||||
|
// temporary file and uses that to remove this file.
|
||||||
|
$this->drupalGet($node->toUrl('edit-form'));
|
||||||
|
switch ($type) {
|
||||||
|
case 'nojs':
|
||||||
|
$this->drupalPostForm(NULL, [$field_name . '[0][fids]' => (string) $victim_tmp_file->id()], 'Remove');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'js':
|
||||||
|
$this->drupalPostAjaxForm(NULL, [$field_name . '[0][fids]' => (string) $victim_tmp_file->id()], ["{$field_name}_0_remove_button" => 'Remove']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The victim's temporary file should not be removed by the attacker's
|
||||||
|
// POST request.
|
||||||
|
$this->assertFileExists($victim_tmp_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,7 +231,13 @@ class UserLoginForm extends FormBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$form_state->setErrorByName('name', $this->t('Unrecognized username or password. <a href=":password">Have you forgotten your password?</a>', array(':password' => $this->url('user.pass', [], array('query' => array('name' => $form_state->getValue('name')))))));
|
// Use $form_state->getUserInput() in the error message to guarantee
|
||||||
|
// that we send exactly what the user typed in. The value from
|
||||||
|
// $form_state->getValue() may have been modified by validation
|
||||||
|
// handlers that ran earlier than this one.
|
||||||
|
$user_input = $form_state->getUserInput();
|
||||||
|
$query = isset($user_input['name']) ? array('name' => $user_input['name']) : array();
|
||||||
|
$form_state->setErrorByName('name', $this->t('Unrecognized username or password. <a href=":password">Have you forgotten your password?</a>', array(':password' => $this->url('user.pass', [], array('query' => $query)))));
|
||||||
$accounts = $this->userStorage->loadByProperties(array('name' => $form_state->getValue('name')));
|
$accounts = $this->userStorage->loadByProperties(array('name' => $form_state->getValue('name')));
|
||||||
if (!empty($accounts)) {
|
if (!empty($accounts)) {
|
||||||
$this->logger('user')->notice('Login attempt failed for %user.', array('%user' => $form_state->getValue('name')));
|
$this->logger('user')->notice('Login attempt failed for %user.', array('%user' => $form_state->getValue('name')));
|
||||||
|
|
|
@ -361,6 +361,30 @@ class UrlHelperTest extends UnitTestCase {
|
||||||
array('//www.drupal.org/foo/bar?foo=bar&bar=baz&baz#foo', TRUE),
|
array('//www.drupal.org/foo/bar?foo=bar&bar=baz&baz#foo', TRUE),
|
||||||
// Internal URL starting with a slash.
|
// Internal URL starting with a slash.
|
||||||
array('/www.drupal.org', FALSE),
|
array('/www.drupal.org', FALSE),
|
||||||
|
// Simple external URLs.
|
||||||
|
array('http://example.com', TRUE),
|
||||||
|
array('https://example.com', TRUE),
|
||||||
|
array('http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo', TRUE),
|
||||||
|
array('//drupal.org', TRUE),
|
||||||
|
// Some browsers ignore or strip leading control characters.
|
||||||
|
array("\x00//www.example.com", TRUE),
|
||||||
|
array("\x08//www.example.com", TRUE),
|
||||||
|
array("\x1F//www.example.com", TRUE),
|
||||||
|
array("\n//www.example.com", TRUE),
|
||||||
|
// JSON supports decoding directly from UTF-8 code points.
|
||||||
|
array(json_decode('"\u00AD"') . "//www.example.com", TRUE),
|
||||||
|
array(json_decode('"\u200E"') . "//www.example.com", TRUE),
|
||||||
|
array(json_decode('"\uE0020"') . "//www.example.com", TRUE),
|
||||||
|
array(json_decode('"\uE000"') . "//www.example.com", TRUE),
|
||||||
|
// Backslashes should be normalized to forward.
|
||||||
|
array('\\\\example.com', TRUE),
|
||||||
|
// Local URLs.
|
||||||
|
array('node', FALSE),
|
||||||
|
array('/system/ajax', FALSE),
|
||||||
|
array('?q=foo:bar', FALSE),
|
||||||
|
array('node/edit:me', FALSE),
|
||||||
|
array('/drupal.org', FALSE),
|
||||||
|
array('<front>', FALSE),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue