diff --git a/core/includes/common.inc b/core/includes/common.inc
index 4a789c8..35802ac 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2894,8 +2894,9 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
// with how render cached output gets stored. This ensures that
// #post_render_cache callbacks get the same data to work with, no matter if
// #cache is disabled, #cache is enabled, there is a cache hit or miss.
- $prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
- $suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
+ $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
+ $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
+
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
// We've rendered this element (and its subtree!), now update the stack.
diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index dc0a6a1..7f8a756 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -7,6 +7,8 @@
namespace Drupal\Component\Utility;
+use Drupal\Component\Utility\Xss;
+
/**
* Manages known safe strings for rendering at the theme layer.
*
@@ -137,6 +139,22 @@ public static function escape($string) {
}
/**
+ * Applies a very permissive XSS/HTML filter for admin-only use.
+ *
+ * @param $string
+ * A string.
+ *
+ * @return string
+ * The escaped string. If $string was already set as safe with
+ * SafeString::set, it won't be escaped again.
+ *
+ * @see \Drupal\Component\Utility\Xss\filterAdmin
+ */
+ public static function checkAdminXss($string) {
+ return static::isSafe($string) ? $string : Xss::filterAdmin($string);
+ }
+
+ /**
* Retrieves all strings currently marked as safe.
*
* This is useful for the batch and form APIs, where it is important to
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index ebab63f..9bdf1f1 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -10,6 +10,7 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\CsrfTokenGenerator;
@@ -791,7 +792,8 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
$element[$key] = $this->doBuildForm($form_id, $element[$key], $form_state);
$count++;
}
-
+ // Add after_build attribute to each element just to check safe markup.
+ $element['#after_build'][] = array(get_class($this), 'formSafeCheck');
// The #after_build flag allows any piece of a form to be altered
// after normal input parsing has been completed.
if (isset($element['#after_build']) && !isset($element['#after_build_done'])) {
@@ -861,6 +863,32 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state
}
/**
+ * Method to ensure every form element pass the safe check.
+ *
+ * @param array $element
+ * - The form element.
+ * @param FormStateInterface $form_state
+ * - The form state of the current form.
+ *
+ * @return array
+ * - The form element marked as safe.
+ */
+ protected function formSafeCheck(array $element, FormStateInterface $form_state) {
+ // Filtering keys which are expected to contain HTML.
+ $markup_keys = array(
+ '#description',
+ '#field_prefix',
+ '#field_suffix',
+ );
+ foreach ($markup_keys as $key) {
+ if (!empty($element[$key]) && is_scalar($element[$key])) {
+ $element[$key] = SafeMarkup::checkAdminXss($element[$key]);
+ }
+ }
+ return $element;
+ }
+
+ /**
* Adds the #name and #value properties of an input element before rendering.
*/
protected function handleInputElement($form_id, &$element, FormStateInterface &$form_state) {
diff --git a/core/lib/Drupal/Core/Render/Element/HtmlTag.php b/core/lib/Drupal/Core/Render/Element/HtmlTag.php
index 851bbf9..5f81e93 100644
--- a/core/lib/Drupal/Core/Render/Element/HtmlTag.php
+++ b/core/lib/Drupal/Core/Render/Element/HtmlTag.php
@@ -139,7 +139,8 @@ public static function preRenderConditionalComments($element) {
$expression = '!IE';
}
else {
- $expression = $browsers['IE'];
+ // Need to filter at least for admin usage.
+ $expression = SafeMarkup::checkAdminXss($browsers['IE']);
}
// Wrap the element's potentially existing #prefix and #suffix properties with
@@ -151,15 +152,23 @@ public static function preRenderConditionalComments($element) {
'#prefix' => '',
'#suffix' => '',
);
+
+ // Ensure what we are dealing with is safe.
+ // This would be done later anyway in drupal_render().
+ $prefix = SafeMarkup::checkAdminXss($element['#prefix']);
+ $suffix = SafeMarkup::checkAdminXss($element['#suffix']);
+
+ // Now calling SafeMarkup::set is safe, because we ensured the
+ // data coming in was at least admin escaped.
if (!$browsers['!IE']) {
// "downlevel-hidden".
- $element['#prefix'] = "\n\n";
+ $element['#prefix'] = SafeMarkup::set("\n\n");
}
else {
// "downlevel-revealed".
- $element['#prefix'] = "\n\n" . $element['#prefix'];
- $element['#suffix'] .= "\n";
+ $element['#prefix'] = SafeMarkup::set("\n\n" . $prefix);
+ $element['#suffix'] = SafeMarkup::set($suffix . "\n");
}
return $element;
diff --git a/core/modules/field_ui/src/Tests/FieldUiTestBase.php b/core/modules/field_ui/src/Tests/FieldUiTestBase.php
index 962acec..314cf5b 100644
--- a/core/modules/field_ui/src/Tests/FieldUiTestBase.php
+++ b/core/modules/field_ui/src/Tests/FieldUiTestBase.php
@@ -105,6 +105,7 @@ function fieldUIAddExistingField($bundle_path, $initial_edit, $field_edit = arra
// First step : 'Re-use existing field' on the 'Manage fields' page.
$this->drupalPostForm("$bundle_path/fields", $initial_edit, t('Save'));
+ $this->assertNoRaw('<', 'The page does not have double escaped HTML tags.');
// Second step : 'Field settings' form.
$this->drupalPostForm(NULL, $field_edit, t('Save settings'));
diff --git a/core/modules/locale/src/Form/ImportForm.php b/core/modules/locale/src/Form/ImportForm.php
index eda1567..d9a811c 100644
--- a/core/modules/locale/src/Form/ImportForm.php
+++ b/core/modules/locale/src/Form/ImportForm.php
@@ -103,18 +103,22 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'file_validate_extensions' => array('po'),
'file_validate_size' => array(file_upload_max_size()),
);
+
+ $file_description = array(
+ '#theme' => 'file_upload_help',
+ '#description' => $this->t('A Gettext Portable Object file.'),
+ '#upload_validators' => $validators,
+ );
+
$form['file'] = array(
'#type' => 'file',
'#title' => $this->t('Translation file'),
- '#description' => array(
- '#theme' => 'file_upload_help',
- '#description' => $this->t('A Gettext Portable Object file.'),
- '#upload_validators' => $validators,
- ),
+ '#description' => drupal_render($file_description),
'#size' => 50,
'#upload_validators' => $validators,
'#attributes' => array('class' => array('file-import-input')),
);
+
$form['langcode'] = array(
'#type' => 'select',
'#title' => $this->t('Language'),
diff --git a/core/modules/options/src/Tests/OptionsFieldUITest.php b/core/modules/options/src/Tests/OptionsFieldUITest.php
index e3b4684..40343bf 100644
--- a/core/modules/options/src/Tests/OptionsFieldUITest.php
+++ b/core/modules/options/src/Tests/OptionsFieldUITest.php
@@ -278,6 +278,7 @@ protected function createOptionsField($type) {
function assertAllowedValuesInput($input_string, $result, $message) {
$edit = array('field_storage[settings][allowed_values]' => $input_string);
$this->drupalPostForm($this->admin_path, $edit, t('Save field settings'));
+ $this->assertNoRaw('<', 'The page does not have double escaped HTML tags.');
if (is_string($result)) {
$this->assertText($result, $message);
diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module
index f407e83..5ffd203 100644
--- a/core/modules/rdf/rdf.module
+++ b/core/modules/rdf/rdf.module
@@ -506,7 +506,7 @@ function rdf_preprocess_comment(&$variables) {
'#theme' => 'rdf_metadata',
'#metadata' => $variables['rdf_metadata_attributes'],
);
- $variables['content']['comment_body']['#prefix'] = drupal_render($rdf_metadata) . $variables['content']['comment_body']['#prefix'];
+ $variables['content']['comment_body']['#prefix'] = SafeMarkup::set(drupal_render($rdf_metadata) . $variables['content']['comment_body']['#prefix']);
}
}
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index d07085c..6ce9eae 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -782,10 +782,10 @@ function testDrupalRenderRenderCachePlaceholder() {
),
),
'#markup' => $placeholder,
- '#prefix' => '
', + '#suffix' => '', ); - $expected_output = '
'; // #cache disabled. $element = $test_element; @@ -826,7 +826,7 @@ function testDrupalRenderRenderCachePlaceholder() { $this->assertIdentical($token, $expected_token, 'The tokens are identical'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '' . $context['bar'] . '
', '#attached' => array(), '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array( @@ -869,11 +869,11 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { ], ], '#markup' => $placeholder, - '#prefix' => '
', + '#suffix' => '' ], ]; - $expected_output = '
' . "\n"; // #cache disabled. $element = $test_element; @@ -917,7 +917,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '' . $context['bar'] . '
', '#attached' => array(), '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array( @@ -943,7 +943,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $this->assertIdentical($token, $expected_token, 'The tokens are identical for the parent element'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '
' . "\n", '#attached' => array(), '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array( @@ -973,7 +973,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() { $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '
', '#attached' => array(), '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array(