From 252f7ffcf2bbbf5bfbd23894a63e44ae9f9d7e54 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Fri, 22 Jun 2018 13:33:12 +0100
Subject: [PATCH] Issue #2652850 by tstoeckler, mohit_aghera, idebr, hchonov,
 mbovan, rodrigoaguilera, jwilson3, pguillard, espurnes, dravenk, ainarend,
 handkerchief, alexpott: Title for details form elements is not set as
 '#markup' and it will be escaped, but all other form elements use '#markup'
 and are not escaped

---
 core/includes/form.inc                        |  5 ++
 core/includes/theme.inc                       |  5 ++
 .../src/Functional/NodeTranslationUITest.php  | 24 +++++++++
 .../modules/form_test/form_test.info.yml      |  3 ++
 .../form_test/src/Form/FormTestLabelForm.php  | 49 +++++++++++++++++++
 .../Functional/Form/ElementsLabelsTest.php    | 12 +++++
 6 files changed, 98 insertions(+)

diff --git a/core/includes/form.inc b/core/includes/form.inc
index d248b7b5a9a9..dacae875bdeb 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -251,6 +251,11 @@ function template_preprocess_details(&$variables) {
     $variables['summary_attributes']['aria-pressed'] = $variables['summary_attributes']['aria-expanded'];
   }
   $variables['title'] = (!empty($element['#title'])) ? $element['#title'] : '';
+  // If the element title is a string, wrap it a render array so that markup
+  // will not be escaped (but XSS-filtered).
+  if (is_string($variables['title']) && $variables['title'] !== '') {
+    $variables['title'] = ['#markup' => $variables['title']];
+  }
   $variables['description'] = (!empty($element['#description'])) ? $element['#description'] : '';
   $variables['children'] = (isset($element['#children'])) ? $element['#children'] : '';
   $variables['value'] = (isset($element['#value'])) ? $element['#value'] : '';
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 30e5aafc6c71..5b9db30831f7 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -570,6 +570,11 @@ function template_preprocess_datetime_wrapper(&$variables) {
 
   if (!empty($element['#title'])) {
     $variables['title'] = $element['#title'];
+    // If the element title is a string, wrap it a render array so that markup
+    // will not be escaped (but XSS-filtered).
+    if (is_string($variables['title']) && $variables['title'] !== '') {
+      $variables['title'] = ['#markup' => $variables['title']];
+    }
   }
 
   // Suppress error messages.
diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
index 46516fe74aad..513c074932e3 100644
--- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
+++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
@@ -501,4 +501,28 @@ public function testRevisionTranslationRendering() {
     $this->assertNoText('First rev en title');
   }
 
+  /**
+   * Test that title is not escaped (but XSS-filtered) for details form element.
+   */
+  public function testDetailsTitleIsNotEscaped() {
+    $this->drupalLogin($this->administrator);
+    // Make the image field a multi-value field in order to display a
+    // details form element.
+    $edit = ['cardinality_number' => 2];
+    $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/storage', $edit, t('Save field settings'));
+
+    // Make the image field non-translatable.
+    $edit = ['settings[node][article][fields][field_image]' => FALSE];
+    $this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
+
+    // Create a node.
+    $nid = $this->createEntity(['title' => 'Node with multi-value image field en title'], 'en');
+
+    // Add a French translation and assert the title markup is not escaped.
+    $this->drupalGet("node/$nid/translations/add/en/fr");
+    $markup = 'Image <span class="translation-entity-all-languages">(all languages)</span>';
+    $this->assertSession()->assertNoEscaped($markup);
+    $this->assertSession()->responseContains($markup);
+  }
+
 }
diff --git a/core/modules/system/tests/modules/form_test/form_test.info.yml b/core/modules/system/tests/modules/form_test/form_test.info.yml
index 39d45eaaf838..fdf1076ef604 100644
--- a/core/modules/system/tests/modules/form_test/form_test.info.yml
+++ b/core/modules/system/tests/modules/form_test/form_test.info.yml
@@ -4,3 +4,6 @@ description: 'Support module for Form API tests.'
 package: Testing
 version: VERSION
 core: 8.x
+dependencies:
+  - file
+  - filter
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestLabelForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestLabelForm.php
index 88de2300898a..a22bf7d09210 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestLabelForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestLabelForm.php
@@ -12,6 +12,40 @@
  */
 class FormTestLabelForm extends FormBase {
 
+  /**
+   * An array of elements that render a title.
+   *
+   * @var array
+   */
+  public static $typesWithTitle = [
+    'checkbox',
+    'checkboxes',
+    'color',
+    'date',
+    'datelist',
+    'datetime',
+    'details',
+    'email',
+    'fieldset',
+    'file',
+    'item',
+    'managed_file',
+    'number',
+    'password',
+    'password_confirm',
+    'radio',
+    'radios',
+    'range',
+    'search',
+    'select',
+    'tel',
+    'textarea',
+    'textfield',
+    'text_format',
+    'url',
+    'weight',
+  ];
+
   /**
    * {@inheritdoc}
    */
@@ -125,6 +159,21 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       ],
       '#required' => TRUE,
     ];
+
+    foreach (static::$typesWithTitle as $type) {
+      $form['form_' . $type . '_title_no_xss'] = [
+        '#type' => $type,
+        '#title' => "$type <script>alert('XSS')</script> is XSS filtered!",
+      ];
+      // Add keys that are required for some elements to be processed correctly.
+      if (in_array($type, ['checkboxes', 'radios'], TRUE)) {
+        $form['form_' . $type . '_title_no_xss']['#options'] = [];
+      }
+      if ($type === 'datetime') {
+        $form['form_' . $type . '_title_no_xss']['#default_value'] = NULL;
+      }
+    }
+
     return $form;
   }
 
diff --git a/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php b/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php
index 6747ed63fb10..91f3ec4a59d4 100644
--- a/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php
+++ b/core/modules/system/tests/src/Functional/Form/ElementsLabelsTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\system\Functional\Form;
 
+use Drupal\form_test\Form\FormTestLabelForm;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -96,6 +97,17 @@ public function testFormLabels() {
     $this->assertTrue(!empty($elements), "Title/Label not displayed when 'visually-hidden' attribute is set in radios.");
   }
 
+  /**
+   * Tests XSS-protection of element labels.
+   */
+  public function testTitleEscaping() {
+    $this->drupalGet('form_test/form-labels');
+    foreach (FormTestLabelForm::$typesWithTitle as $type) {
+      $this->assertSession()->responseContains("$type alert('XSS') is XSS filtered!");
+      $this->assertSession()->responseNotContains("$type <script>alert('XSS')</script> is XSS filtered!");
+    }
+  }
+
   /**
    * Tests different display options for form element descriptions.
    */
-- 
GitLab