-
Patch for Drupal 8.7.14 SA-CORE-2020-004, SA-CORE-2020-005, and SA-CORE-2020-006
I have a site that’s temporarily stuck on 8.7.14; it’s not worth the risk to update to 8.8.8 right now.
I was able to diff 8.8.7 and 8.8.8 to figure out what changes were made for these security announcements:
- Drupal core – Critical – Cross-Site Request Forgery – SA-CORE-2020-004
- Drupal core – Critical – Arbitrary PHP code execution – SA-CORE-2020-005
- Drupal core – Less critical – Access bypass – SA-CORE-2020-006
I could then compare the changes with 8.7.14 to create the patch below, which applies against 8.7.14 without any issues.
DISCLAIMER: The formal recommendation is to update to 8.8.8 if at all possible. Use the patch below at your own risk.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.incindex 348724d6..e9b7cbc1 100644--- a/core/includes/bootstrap.inc+++ b/core/includes/bootstrap.inc@@ -673,11 +673,17 @@ function drupal_valid_test_ua($new_prefix = NULL) {// Ensure that no information leaks on production sites.$test_db = new TestDatabase($prefix);$key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';- if (!is_readable($key_file)) {+ if (!is_readable($key_file) || is_dir($key_file)) {header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');exit;}$private_key = file_get_contents($key_file);+ // The string from drupal_generate_test_ua() is 74 bytes long. If we don't+ // have it, tests cannot be allowed.+ if (empty($private_key) || strlen($private_key) < 74) {+ header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');+ exit;+ }// The file properties add more entropy not easily accessible to others.$key = $private_key . filectime(__FILE__) . fileinode(__FILE__);$time_diff = REQUEST_TIME - $time;diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.phpindex 4bf64196..7bacc473 100644--- a/core/lib/Drupal/Core/Form/FormBuilder.php+++ b/core/lib/Drupal/Core/Form/FormBuilder.php@@ -18,6 +18,7 @@use Drupal\Core\Theme\ThemeManagerInterface;use Symfony\Component\EventDispatcher\EventDispatcherInterface;use Symfony\Component\HttpFoundation\FileBag;+use Symfony\Component\HttpFoundation\ParameterBag;use Symfony\Component\HttpFoundation\RequestStack;use Symfony\Component\HttpFoundation\Response;@@ -955,8 +956,16 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state// This value is checked in self::handleInputElement().$form_state->setInvalidToken(TRUE);+ // Ignore all submitted values.+ $form_state->setUserInput([]);++ $request = $this->requestStack->getCurrentRequest();+ // Do not trust any POST data.+ $request->request = new ParameterBag();// Make sure file uploads do not get processed.- $this->requestStack->getCurrentRequest()->files = new FileBag();+ $request->files = new FileBag();+ // Ensure PHP globals reflect these changes.+ $request->overrideGlobals();}}}diff --git a/core/lib/Drupal/Core/Form/FormValidator.php b/core/lib/Drupal/Core/Form/FormValidator.phpindex 57d24cc2..2afcb52b 100644--- a/core/lib/Drupal/Core/Form/FormValidator.php+++ b/core/lib/Drupal/Core/Form/FormValidator.php@@ -124,10 +124,8 @@ public function validateForm($form_id, &$form, FormStateInterface &$form_state)* {@inheritdoc}*/public function setInvalidTokenError(FormStateInterface $form_state) {- $url = $this->requestStack->getCurrentRequest()->getRequestUri();-// Setting this error will cause the form to fail validation.- $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Copy any unsaved work in the form below and then <a href=":link">reload this page</a>.', [':link' => $url]));+ $form_state->setErrorByName('form_token', $this->t('The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.'));}/**diff --git a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.phpindex 0b6254d8..db8e7fbe 100644--- a/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php+++ b/core/modules/file/tests/src/Functional/FileManagedFileElementTest.php@@ -45,7 +45,7 @@ public function testManagedFile() {$file_field_name => \Drupal::service('file_system')->realpath($test_file->getFileUri()),];$this->drupalPostForm(NULL, $edit, t('Save'));- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');+ $this->assertText('The form has become outdated.');$last_fid = $this->getLastFileId();$this->assertEqual($last_fid_prior, $last_fid, 'File was not saved when uploaded with an invalid form token.');diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.phpindex 5c43d3ee..e21903c2 100644--- a/core/modules/jsonapi/src/Controller/EntityResource.php+++ b/core/modules/jsonapi/src/Controller/EntityResource.php@@ -315,7 +315,7 @@ public function patchIndividual(ResourceType $resource_type, EntityInterface $en));}$data += ['attributes' => [], 'relationships' => []];- $field_names = array_merge(array_keys($data['attributes']), array_keys($data['relationships']));+ $field_names = array_map([$resource_type, 'getInternalName'], array_merge(array_keys($data['attributes']), array_keys($data['relationships'])));array_reduce($field_names, function (EntityInterface $destination, $field_name) use ($resource_type, $parsed_entity) {$this->updateEntityField($resource_type, $parsed_entity, $destination, $field_name);diff --git a/core/modules/jsonapi/src/Controller/FileUpload.php b/core/modules/jsonapi/src/Controller/FileUpload.phpindex b07dd51d..f8517fd7 100644--- a/core/modules/jsonapi/src/Controller/FileUpload.php+++ b/core/modules/jsonapi/src/Controller/FileUpload.php@@ -112,6 +112,7 @@ public function __construct(AccountInterface $current_user, EntityFieldManagerIn* created file entity.*/public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, $file_field_name, FieldableEntityInterface $entity) {+ $file_field_name = $resource_type->getInternalName($file_field_name);$field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity);@@ -138,7 +139,7 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy$entity->save();$route_parameters = ['entity' => $entity->uuid()];- $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $file_field_name);+ $route_name = sprintf('jsonapi.%s.%s.related', $resource_type->getTypeName(), $resource_type->getPublicName($file_field_name));$related_url = Url::fromRoute($route_name, $route_parameters)->toString(TRUE);$request = Request::create($related_url->getGeneratedUrl(), 'GET', [], $request->cookies->all(), [], $request->server->all());return $this->httpKernel->handle($request, HttpKernelInterface::SUB_REQUEST);@@ -161,6 +162,7 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy* Thrown when there are validation errors.*/public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, $file_field_name) {+ $file_field_name = $resource_type->getInternalName($file_field_name);$field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name);static::ensureFileUploadAccess($this->currentUser, $field_definition);@@ -182,7 +184,7 @@ public function handleFileUploadForNewResource(Request $request, ResourceType $r/* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */$links = new LinkCollection(['self' => $self_link]);- $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($file_field_name);+ $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($resource_type->getPublicName($file_field_name));$file_resource_type = reset($relatable_resource_types);$resource_object = ResourceObject::createFromEntity($file_resource_type, $file);return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []);diff --git a/core/modules/system/tests/src/Functional/Form/FormTest.php b/core/modules/system/tests/src/Functional/Form/FormTest.phpindex 849944b0..62ae3c54 100644--- a/core/modules/system/tests/src/Functional/Form/FormTest.php+++ b/core/modules/system/tests/src/Functional/Form/FormTest.php@@ -244,21 +244,27 @@ public function testInputWithInvalidToken() {$this->assertSession()->elementExists('css', 'input[name="form_token"]')->setValue('invalid token');+ $random_string = $this->randomString();$edit = [- 'textfield' => $this->randomString(),+ 'textfield' => $random_string,'checkboxes[bar]' => TRUE,'select' => 'bar','radios' => 'foo',];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- // Verify that input elements retained the posted values.- $this->assertFieldByName('textfield', $edit['textfield']);++ $assert = $this->assertSession();+ $element = $assert->fieldExists('textfield');+ $this->assertEmpty($element->getValue());+ $assert->responseNotContains($random_string);+ $this->assertText('The form has become outdated.');+ // Ensure that we don't use the posted values.+ $this->assertFieldByName('textfield', '');$this->assertNoFieldChecked('edit-checkboxes-foo');- $this->assertFieldChecked('edit-checkboxes-bar');- $this->assertOptionSelected('edit-select', 'bar');- $this->assertFieldChecked('edit-radios-foo');+ $this->assertNoFieldChecked('edit-checkboxes-bar');+ $this->assertOptionSelected('edit-select', '');+ $this->assertNoFieldChecked('edit-radios-foo');// Check another form that has a textarea input.$this->drupalGet(Url::fromRoute('form_test.required'));@@ -271,9 +277,9 @@ public function testInputWithInvalidToken() {];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('textfield', $edit['textfield']);- $this->assertFieldByName('textarea', $edit['textarea']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('textfield', '');+ $this->assertFieldByName('textarea', '');// Check another form that has a number input.$this->drupalGet(Url::fromRoute('form_test.number'));@@ -281,12 +287,14 @@ public function testInputWithInvalidToken() {->elementExists('css', 'input[name="form_token"]')->setValue('invalid token');$edit = [- 'integer_step' => mt_rand(1, 100),+ // We choose a random value which is higher than the default value,+ // so we don't accidentally generate the default value.+ 'integer_step' => mt_rand(6, 100),];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('integer_step', $edit['integer_step']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('integer_step', 5);// Check a form with a Url field$this->drupalGet(Url::fromRoute('form_test.url'));@@ -298,8 +306,9 @@ public function testInputWithInvalidToken() {];$this->drupalPostForm(NULL, $edit, 'Submit');$this->assertFieldByXpath('//div[contains(@class, "error")]', NULL, 'Error message is displayed with invalid token even when required fields are filled.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');- $this->assertFieldByName('url', $edit['url']);+ $this->assertText('The form has become outdated.');+ $this->assertFieldByName('url', '');+}/**diff --git a/core/modules/system/tests/src/Functional/Form/ValidationTest.php b/core/modules/system/tests/src/Functional/Form/ValidationTest.phpindex 094a0af7..50451a98 100644--- a/core/modules/system/tests/src/Functional/Form/ValidationTest.php+++ b/core/modules/system/tests/src/Functional/Form/ValidationTest.php@@ -68,7 +68,7 @@ public function testValidate() {$this->drupalPostForm(NULL, ['name' => 'validate'], 'Save');$this->assertNoFieldByName('name', '#value changed by #validate', 'Form element #value was not altered.');$this->assertNoText('Name value: value changed by setValueForElement() in #validate', 'Form element value in $form_state was not altered.');- $this->assertText('The form has become outdated. Copy any unsaved work in the form below');+ $this->assertText('The form has become outdated.');}/**diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.phpindex 8d20db5c..d2dec0c6 100644--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php@@ -799,12 +799,30 @@ public function testInvalidToken($expected, $valid_token, $user_is_authenticated$expected_form = $form_id();$form_arg = $this->getMockForm($form_id, $expected_form);+ // Set up some request data so we can be sure it is removed when a token is+ // invalid.+ $this->request->request->set('foo', 'bar');+ $_POST['foo'] = 'bar';+$form_state = new FormState();$input['form_id'] = $form_id;$input['form_token'] = $form_token;+ $input['test'] = 'example-value';$form_state->setUserInput($input);- $this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);+ $form = $this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);$this->assertSame($expected, $form_state->hasInvalidToken());+ if ($expected) {+ $this->assertEmpty($form['test']['#value']);+ $this->assertEmpty($form_state->getValue('test'));+ $this->assertEmpty($_POST);+ $this->assertEmpty(iterator_to_array($this->request->request->getIterator()));+ }+ else {+ $this->assertEquals('example-value', $form['test']['#value']);+ $this->assertEquals('example-value', $form_state->getValue('test'));+ $this->assertEquals('bar', $_POST['foo']);+ $this->assertEquals('bar', $this->request->request->get('foo'));+ }}public function providerTestInvalidToken() {diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.phpindex ef08ab77..36605065 100644--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php@@ -173,7 +173,7 @@ protected function setUp() {->getMock();$this->account = $this->getMock('Drupal\Core\Session\AccountInterface');$this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');- $this->request = new Request();+ $this->request = Request::createFromGlobals();$this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');$this->requestStack = new RequestStack();$this->requestStack->push($this->request);diff --git a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.phpindex 18f0be28..10f54905 100644--- a/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php+++ b/core/tests/Drupal/Tests/Core/Form/FormValidatorTest.php@@ -131,7 +131,7 @@ public function testValidateInvalidFormToken() {->getMock();$form_state->expects($this->once())->method('setErrorByName')- ->with('form_token', 'The form has become outdated. Copy any unsaved work in the form below and then <a href="/test/example?foo=bar">reload this page</a>.');+ ->with('form_token', 'The form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.');$form_state->setValue('form_token', 'some_random_token');$form_validator->validateForm('test_form_id', $form, $form_state);$this->assertTrue($form_state->isValidationComplete());