Problem/Motivation

drupal_set_message is a little magic (because security). if the result of the t() function has a single unescaped value (e.g. !logout), then it will cause the entire message to be marked as "unsafe", which causes the message to be escaped on output. This results in images like the one below, taken after a one-time login link for the user "joe" was used while the "admin" user was logged in:

One-time login link when another user already logged in.

Proposed resolution

The solution to this problem is to use @login instead; this escapes the login link, which keeps the message safe. The result of this change looks like this:

Remaining tasks

As observed below, using !message with drupal_set_message will always result in double-escaping. There are not too many other places in Drupal where this pattern is used; these should all be changed to @message.

It would probably be a good idea to update the documentation of drupal_set_message to advise that this pattern should not be used.

!message might also produce double escaping when used with other methods. This should probably be a follow-on issue or issues, though.

Comments

greg.1.anderson’s picture

Issue summary: View changes
Status: Active » Needs review
StatusFileSize
new1.96 KB

Here is a patch to fix the two places where this occurs in the User module.

greg.1.anderson’s picture

Issue tags: +Novice
tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Quickfix

I don't think this is worth an automated test, but a committer might disagree with me.

edutrul’s picture

@langelhc, @AlexaBR @edutrul have done some grep into drupal core and found is fine about escaping output.


./core/modules/field_ui/src/Form/FieldStorageAddForm.php:366:        drupal_set_message($this->t('There was a problem creating field %label: !message', array('%label' => $values['label'], '!message' => $e->getMessage())), 'error');


./core/modules/field_ui/src/Form/FieldStorageAddForm.php:397:        drupal_set_message($this->t('There was a problem creating field %label: !message', array('%label' => $values['label'], '!message' => $e->getMessage())), 'error');
./core/modules/views/src/DisplayPluginCollection.php:88:      drupal_set_message(t('!message', array('!message' => $message)), 'warning');
./core/modules/file/tests/file_test/src/Form/FileTestForm.php:118:      drupal_set_message(t('You WIN!'));
./core/modules/file/tests/file_test/src/Form/FileTestForm.php:121:      drupal_set_message(t('Epic upload FAIL!'), 'error');
./core/modules/config/src/Form/ConfigSync.php:188:      drupal_set_message($this->t('Your current configuration has changed. Changes to these configuration items will be lost on the next synchronization: !changes', array('!changes' => $change_list_html)), 'warning');

RTBC #drupalconlatino

greg.1.anderson’s picture

Thanks for doing further research into this issue, @edutrul.

Instances where "!" are used when there is no html in the message are okay, because it is innocuous to check_plain() text that is already plain. The first two you quote might sometimes be an issue, though, because %label will be escaped, which will cause double-escaping due to "!message" if the contents of $values['label'] contains any content that is altered when escaped. So, these two could probably become @message.

greg.1.anderson’s picture

So, here is a screenshot after adding Joe's "special" field. I wasn't sure how to force the exception to be thrown, so I temporarily added a drupal_set_message() to the non-failure case just to take the screenshot.

Temporary code (wrong, %label is double-escapped):

        drupal_set_message($this->t('There was no problem creating field %label: !message', array('%label' => $values['label'], '!message' => "This is just a test.")));

Screenshot:

Double escaping %label

I've added an updated patch to also correct this issue.

greg.1.anderson’s picture

Title: drupal_set_message output escaping in UserController is not correct » !message in t() always leads to double-escaping when used in drupal_set_message()
Issue summary: View changes
Status: Reviewed & tested by the community » Needs work
StatusFileSize
new5.26 KB

I was thinking about this overnight, and realized that using '!message' with drupal_set_message is always a problem in Drupal 8. To demonstrate this, I ran the following test:

drupal_set_message($this->t('Test of bang substitutions: !message', array('!message' => 'This is a test')));

That produced the following output:

Drupal set message demo

So, !message works the same as @message, but !message has the additional undesirable side-effect of escaping other parts of the message unrelated to the substitution that used "!". !message is therefore only usable in cases where there is no content that would be altered by output escaping. In that instance, though, @message works just as well. Therefore, the other places in Drupal 8 where this pattern occurs should be changed to use @message instead of !message, to avoid the side-effect, and reduce the chance that this pattern might spread through emulation. For example, if someone copies a druap_set_message that uses !message, and then adds an @other variable, then a double-escaping bug will be introduced. This can linger if the message comes up only rarely, or if the substituted values usually do not contain any characters that need escaping.

xjm’s picture

greg.1.anderson’s picture

Issue tags: +SafeMarkup
greg.1.anderson’s picture

Status: Needs work » Needs review
StatusFileSize
new11.99 KB
new3.91 KB

Okay, I made an important realization: !message is useful and works as intended, but only if the replacement value is already marked as a safe string.

Here is a working example from Drupal 8 core, in core/modules/config/src/Form/ConfigSync.php buildForm():

      $change_list_render = array(
        '#theme' => 'item_list',
        '#items' => $change_list,
      );
      $change_list_html = drupal_render($change_list_render);
      drupal_set_message($this->t('Your current configuration has changed. Changes to these configuration items will be lost on the next synchronization: !changes', array('!changes' => $change_list_html)), 'warning');

Here is a screenshot of a copy of this code, executed with a hardcoded list of two items, "item 1" and "item 2":

Using safe data with drupal_set_message

So, the output of drupal_render() is marked as "safe", so when this safe value is used as the replacement value for !changes, above, the result of t() is also marked as safe, and no double-escaping occurs. This is different than the erroneous values above, because in the failure cases, the replacement values were simple strings that had never passed through any function that marked them safe with SafeMarkup::set(). Calling SafeMarkup::set() directly is deprecated, so the usage model for using !replacement with drupal_set_message() is to always insure that the replacement value came from drupal_render(), or some other function that will mark the result safe.

There was only one other place in Drupal core where a !replacement needed to be changed to @replacement. I have attached an updated patch that fixes it, plus #6 and #1.

greg.1.anderson’s picture

Title: !message in t() always leads to double-escaping when used in drupal_set_message() » !message in t() can lead to double-escaping when used in drupal_set_message()
Jeff Burnz’s picture

greg.1.anderson ++, thanks for all the snooping, you saved me loads of time, cheers :)

subhojit777’s picture

Status: Needs review » Reviewed & tested by the community

Code changes looks good and the patch fixes the problem. I agree with @tim.plunkett in #3, it is a small fix and does not needs tests. @greg.1.anderson thank you for keeping up the work in this.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue is a normal bug fix, and doesn't include any disruptive changes, so it is allowed per https://www.drupal.org/core/beta-changes. Committed 13d4cd2 and pushed to 8.0.x. Thanks!

  • alexpott committed 17a23d9 on 8.0.x
    Issue #2425581 by greg.1.anderson: !message in t() can lead to double-...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.