Problem/Motivation

In Drupal 8 we enabled twig autoescape but we also created SafeMarkup::checkPlain() to escape text and then mark it as safe to ensure that Twig does not double escape it.

Proposed resolution

We should rely on Twig auto escape and completely remove SafeMarkup::checkPlain(). SafeMarkup::checkPlain() is dangerous because if the resulting string is modified in any way, for example nl2br(), the result will no longer be marked safe. In places where explicit escaping is needed, Html::escape() should be used.

User interface changes

None

API changes

Remove SafeMarkup::checkPlain() completely.

Data model changes

None

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because we have double escaping in HEAD because of incorrect SafeMarkup::checkPlain() usage
Issue priority Major because SafeMarkup::checkPlain() is hard to use correctly and we should be using auto escape
Disruption Disruptive but we have announced this on d.o see #2555473: Deprecate SafeMarkup::checkPlain() for Drupal 8.0.x

Comments

alexpott created an issue. See original summary.

alexpott’s picture

dawehner’s picture

Status: Needs review » Active
alexpott’s picture

Title: Remove all code usages SafeMarkup::checkPlain() and rely more on Twig autoescaping » Remove SafeMarkup::checkPlain()
alexpott’s picture

alexpott’s picture

Status: Postponed » Needs review
StatusFileSize
new9.8 KB

Status: Needs review » Needs work

The last submitted patch, 6: 2569699-6.patch, failed testing.

Status: Needs work » Needs review

alexpott queued 6: 2569699-6.patch for re-testing.

lauriii’s picture

Status: Needs review » Reviewed & tested by the community

The patch looks good. So long SafeMarkup::checkPlain() \o/

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

@lauriii did you read the batch documentation in form.inc - did it make sense to you? In the previous both @dawehner and @xjm expressed concern over the docs.

lauriii’s picture

Status: Needs review » Needs work

Sorry I didn't see the comments on the previous issue. After reading it again I agree that it could be more clear and the wording could be changed to be better. Maybe if there is specific comments about there, we could copy the feedback here.

joelpittet’s picture

  1. +++ b/core/includes/form.inc
    @@ -574,10 +574,13 @@ function template_preprocess_form_element_label(&$variables) {
    - * returns any user input in the 'results' or 'message' keys of $context, it
    - * must also sanitize them first.
    + * returns any user input in the 'message' key of $context, it must also
    + * sanitize it first. The 'result' key is not displayed to the user and
    + * therefore it is responsibility of the batch callbacks to handle sanitization
    + * if required. If the 'result' key is being themed using item_list the callback
    + * can rely on Twig auto-escape.
    

    Is there a 'result' key or is this 'results'?

    Can we get away with not sanitizing it first?

    Yes this sounds a bit contradictory. It's using item list but yet still needs to be manually escaped by the callback. Is this a JS issue maybe?

  2. +++ b/core/includes/form.inc
    @@ -621,10 +627,13 @@ function template_preprocess_form_element_label(&$variables) {
    - *     $context['results'][] = $row->id . ' : ' . SafeMarkup::checkPlain($row->title);
    + *     // This can be used by the batch finished callback. If this is displayed to
    + *     // the user via the item list theme then it will be auto-escaped.
    + *     $context['results'][] = $row->id . ' : ' . $row->title;
    

    This sounds like if the title was using t('@place') in the title then it would double escape by the auto-escape.

    Also nit: 80 chars

  3. +++ b/core/includes/form.inc
    @@ -621,10 +627,13 @@ function template_preprocess_form_element_label(&$variables) {
    - *     $context['message'] = SafeMarkup::checkPlain($row->title);
    + *     // This is used in JavaScript and should be escaped.
    + *     $context['message'] = Html::escape($row->title);
    

    Why isn't it being auto-escaped automatically like the 'results'?

  4. +++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
    @@ -15,9 +15,13 @@
    + * - Strings sanitized by self::format() unless a !placeholder argument is not
    + *   already marked safe.
    

    Just a note: !placeholder may not be around for long but for now this is safe to say.

joelpittet’s picture

imiksu’s picture

Assigned: Unassigned » imiksu
joelpittet’s picture

Discussing the batch API with @alexpott IRL. We may be able to check safe variables and escape "just before" sending to the JavaScript. Ideally we would do this in JavaScript but this may be a nice DX compromise and would be slightly easier if we get an idea on how to deal with safe strings in JS in the future.

imiksu’s picture

Assigned: imiksu » Unassigned
StatusFileSize
new9.75 KB
new1.81 KB

I decided to not touch yet on sanitization part of code as I'm not familiar enough with batch API, but I worked on following comments in #12:

Is there a 'result' key or is this 'results'?

Confirmed that and changed

Can we get away with not sanitizing it first?

Took off the whole sentence

Also nit: 80 chars

Nailed it

Just a note: !placeholder may not be around for long but for now this is safe to say.

I couldn't find better rephrasing for this, so I decided to leave it as is for now

mr.baileys’s picture

Assigned: Unassigned » mr.baileys

Going to take a stab at moving this forward.

lauriii’s picture

lauriii’s picture

Assigned: mr.baileys » Unassigned
Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 16: 2569699-16.patch, failed testing.

The last submitted patch, 16: 2569699-16.patch, failed testing.

lauriii’s picture

Status: Needs work » Needs review
StatusFileSize
new9.73 KB

Reroll of the latest patch.

joelpittet’s picture

Status: Needs review » Needs work
  1. +++ b/core/includes/form.inc
    @@ -601,8 +602,11 @@ function template_preprocess_form_element_label(&$variables) {
    - *   $context['results'][] = $node->id() . ' : ' . SafeMarkup::checkPlain($node->label());
    - *   $context['message'] = SafeMarkup::checkPlain($node->label());
    + *   // This can be used by the batch finished callback. If this is displayed to
    + *   // the user via the item list theme then it will be auto-escaped.
    + *   $context['results'][] = $node->id() . ' : ' . $node->label();
    + *   // This is used in JavaScript and should be escaped.
    + *   $context['message'] = Html::escape($node->label());
    
    +++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
    @@ -130,36 +137,6 @@ public static function getAll() {
    -    static::$safeStrings[$string]['html'] = TRUE;
    

    So discussed briefly with @alexpott over the weekend and to avoid explaining what the finished batch API callback should/shouldn't do to variables we should just check the SafeStringInterface and escape if not safe otherwise just before it's sent to JavaScript, similar to what twig autoescape does.

    Ultimately JS wouldn't know of SafeStringInterface and escape it... but I think this is the best solution.

  2. +++ b/core/includes/form.inc
    @@ -621,10 +625,13 @@ function template_preprocess_form_element_label(&$variables) {
    + *     // This is used in JavaScript and should be escaped.
    + *     $context['message'] = Html::escape($row->title);
    

    Same, avoid this and check and escape if unsafe.

joelpittet’s picture

#23 is the same as #15

lauriii’s picture

Status: Needs work » Needs review
StatusFileSize
new9.83 KB
new1.08 KB

Trying to address #23 :)

joelpittet’s picture

StatusFileSize
new4.33 KB
new11.36 KB

Here's what I was proposing, hopefully it does the trick.

Status: Needs review » Needs work

The last submitted patch, 26: remove-2569699-26.patch, failed testing.

The last submitted patch, 26: remove-2569699-26.patch, failed testing.

lauriii’s picture

Status: Needs work » Needs review
StatusFileSize
new12.68 KB
new1.28 KB

This should fix the tests

alexpott’s picture

+++ b/core/includes/batch.inc
@@ -102,6 +103,14 @@ function _batch_do() {
+  if (!SafeMarkup::isSafe($message)) {
+    $message = Html::escape($message);
+  }

Is it time to add SafeMarkup::escapeIfUnsafe() it seems we have quite a few issues that need this.

novitsh’s picture

Tested patch from #29. No issues found. RTBC?

imiksu’s picture

dawehner’s picture

StatusFileSize
new3.11 KB
new12.09 KB

Rerolling the patch, added some documentation and working on the test coverage now.

lauriii’s picture

+++ b/core/includes/batch.inc
@@ -315,6 +341,15 @@ function _batch_process() {
+    // escaped accidentally, which would be more complex than escaping plain text

80 chars. The patch looks good but I don't think Im really qualified to RTBC this so leaving to someone else..

alexpott’s picture

Status: Needs review » Needs work
+++ b/core/includes/form.inc
@@ -571,14 +572,6 @@ function template_preprocess_form_element_label(&$variables) {
- * Note: if the batch 'title', 'init_message', 'progress_message', or
- * 'error_message' could contain any user input, it is the responsibility of
- * the code calling batch_set() to sanitize them first with a function like
- * \Drupal\Component\Utility\SafeMarkup::checkPlain() or
- * \Drupal\Component\Utility\Xss::filter(). Furthermore, if the batch operation
- * returns any user input in the 'results' or 'message' keys of $context, it
- * must also sanitize them first.

I think we need to document that regardless of where the output ends up it will autoescape.

stefan.r’s picture

  1. +++ b/core/includes/batch.inc
    @@ -102,6 +103,17 @@ function _batch_do() {
    +  // the javascript layer would be more complex, because we would need to
    

    s/javascript/JavaScript/

  2. +++ b/core/includes/batch.inc
    @@ -102,6 +103,17 @@ function _batch_do() {
    +  if (!SafeMarkup::isSafe($message)) {
    +    $message = Html::escape($message);
    +  }
    +  if (!SafeMarkup::isSafe($label)) {
    +    $label = Html::escape($label);
    +  }
    

    +1

  3. +++ b/core/includes/batch.inc
    @@ -167,10 +179,24 @@ function _batch_progress_page() {
    +  // the javascript layer would be more complex, because we would need to
    

    s/javascript/JavasScript/

  4. +++ b/core/includes/batch.inc
    @@ -315,6 +341,15 @@ function _batch_process() {
    +    // the javascript layer would be more complex, because we would need to
    

    s/javascript/JavaScript/

  5. +++ b/core/includes/form.inc
    @@ -571,14 +572,6 @@ function template_preprocess_form_element_label(&$variables) {
    - * Note: if the batch 'title', 'init_message', 'progress_message', or
    - * 'error_message' could contain any user input, it is the responsibility of
    - * the code calling batch_set() to sanitize them first with a function like
    - * \Drupal\Component\Utility\SafeMarkup::checkPlain() or
    - * \Drupal\Component\Utility\Xss::filter(). Furthermore, if the batch operation
    - * returns any user input in the 'results' or 'message' keys of $context, it
    - * must also sanitize them first.
    - *
    

    Yay!

  6. +++ b/core/includes/form.inc
    @@ -749,7 +744,7 @@ function batch_set($batch_definition) {
    +    $batch_set['init_message'] = new FormattableString('@message<br>&nbsp;', ['@message' => $batch_set['init_message']]);;
    

    We want the mesage to be always escaped, right?

  7. +++ b/core/includes/form.inc
    @@ -749,7 +744,7 @@ function batch_set($batch_definition) {
     
    

    s:<br>:<br />:

  8. +++ b/core/modules/system/src/Tests/Batch/PageTest.php
    @@ -68,7 +68,7 @@ function testBatchProgressMessages() {
    +    $this->assertRaw('<div class="progress__description">Initializing.<br>&nbsp;</div>', 'Initial progress message appears correctly.');
    

    s:<br>:<br />:

  9. +++ b/core/includes/form.inc
    @@ -601,8 +594,8 @@ function template_preprocess_form_element_label(&$variables) {
    - *   $context['results'][] = $node->id() . ' : ' . SafeMarkup::checkPlain($node->label());
    - *   $context['message'] = SafeMarkup::checkPlain($node->label());
    + *   $context['results'][] = $node->id() . ' : ' . $node->label();
    + *   $context['message'] = $node->label();
    

    +1, should we also document the escaping behavior here?

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new12.33 KB
new6.25 KB

Thank you for all your reviews!

We want the mesage to be always escaped, right?

Well it will be, through the usage of the "@" placeholder.

+1, should we also document the escaping behavior here?

Did earlier.

Upload the current batch.

stefan.r’s picture

  1. +++ b/core/includes/form.inc
    @@ -572,6 +572,12 @@ function template_preprocess_form_element_label(&$variables) {
    + * Note: The batch 'title', 'init_message', 'progress_message', 'error_message'
    + * and the keys 'results' and 'message' in $context might contain any user
    + * input. If the passed in value is not a safe string (like the result of a
    + * valid t() call), then the value will be escaped and displayed as such by the
    + * batch JavaScript.
    

    How about "Note: The following elements might contain user input:
    - $batch['title']
    - $batch['init_message']
    - $batch['progress_message']
    - $batch['error_message']
    - $context['results']
    - $context['messaeg']
    If the passed in value is not a SafeStringInterface object (such as an object returned from a valid t() call), then this value will be escaped an the batch JavaScript will display it as escaped as well."

  2. +++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
    @@ -130,6 +130,36 @@ public static function getAll() {
    +  public static function checkPlain($text) {
    

    :(

  3. +++ b/core/modules/system/src/Tests/Batch/PageTest.php
    @@ -76,4 +78,17 @@ function testBatchProgressMessages() {
    +    $this->drupalGet('batch-test/test-title');
    ...
    +    $this->drupalGet($url->__toString());
    

    Do we want to assert anything here or merely check that the drupalGet is successful?

Status: Needs review » Needs work

The last submitted patch, 37: 2569699-37.patch, failed testing.

The last submitted patch, 37: 2569699-37.patch, failed testing.

alexpott’s picture

As much as I would love to remove SafeMarkup::checkPlain() - it is actually unnecessary to achieve the goal of removing the safe string list. We can just have it return an object the implements SafeStringInterface. See #2575615: Introduce HtmlEscapedText and remove SafeMarkup::setMultiple() and SafeMarkup::getAll() and remove the static safeStrings list for an implementation that also completely removes the setter and getter functions for the safe list.

Given the fact that the disruption of removing SafeMarkup::checkPlain() for existing contrib and custom modules and given the acceptable work around I think we should postpone removal until Drupal 9.

alexpott’s picture

Can people who have contributed to this patch please comment on #2575615: Introduce HtmlEscapedText and remove SafeMarkup::setMultiple() and SafeMarkup::getAll() and remove the static safeStrings list so they can receive credit for their work - thanks.

novitsh’s picture

@alexpott: Does that also count for reviewing?

Can this issue be closed?

alexpott’s picture

@Novitsh we will remove SafeMarkup::checkPlain() in Drupal 9 - we can do that on this issue.

stefan.r’s picture

Title: Remove SafeMarkup::checkPlain() » Remove SafeMarkup::checkPlain() for Drupal 9.0.x
novitsh’s picture

@alexpott, @stefan.r ok for me.

catch’s picture

Status: Postponed » Closed (duplicate)

Version: 9.x-dev » 9.0.x-dev

The 9.0.x branch will open for development soon, and the placeholder 9.x branch should no longer be used. Only issues that require a new major version should be filed against 9.0.x (for example, removing deprecated code or updating dependency major versions). New developments and disruptive changes that are allowed in a minor version should be filed against 8.9.x, and significant new features will be moved to 9.1.x at committer discretion. For more information see the Allowed changes during the Drupal 8 and 9 release cycles and the Drupal 9.0.0 release plan.