2015-09-29 After a call with @effulgentsia, @catch, @davereid, @lauriii, @Berdir, @alexpott, @dawehner, @stefan.r, and @webchick (thanks to @lauriii for organizing this!) the following conclusions were reached.
Problem/Motivation
Numerous parts of the Token API take an $options array, one of which is sanitize: "A boolean flag indicating that tokens should be sanitized for display to a web browser. Defaults to TRUE." In HEAD as well as D7, $options['sanitize'] refers to the token, not the input.
In many cases this is handled wrong; for example, a node body is never sanitized or unsanitized if it's using a text format - callers need to run the text format either way since the unprocessed value is completely meaningless.
These Token API elements that take the sanitize option are as follows:
- hook_tokens(), the API via which modules declare the replacement values for tokens they define in hook_token_info().
- hook_tokens_alter(), which allows other modules to modify these return values.
- Token::generate(), which invokes hook_tokens() and hook_tokens_alter().
- Token::replace(), the main method responsible for taking a tokenized string and replacing the token values in it, which calls Token::generate().
Use Cases
There are use cases for the Token API in both core (e-mail templates, field descriptions, file directories) and contrib (Pathauto, SimpleNews, Token Filter). In all identified cases, either HTML or plain text is desired.
- Taking a plain text string, replacing the arguments, then using the result as plain text. Examples are core's email templates, the Pathauto module, and filefield directory paths. PlainTextOutput::renderFromHtml() works great on converting HTML to plain text for these use cases.
- Taking a plain text string, replacing the arguments, then using the result in an HTML context. For example:
Token::replace(‘Hello [user:name], thanks for posting [node:title]’);- node title needs to be escaped, but there is no HTML in the passed in string, so the passed in string can also be escaped. - Taking an HTML string, replacing the arguments, then using the result in an HTML context. Examples are core's field descriptions, and Simplenews module's HTML emails. For example:
Token::replace(‘Hello [user:name] thanks for posting <a href=”[node:url]” title=”[node:title]”>[node:title]</a>’);
For #1 and #2, replacement and conversion to plain text are simple.
For #3, Token::replace() currently does not take into account the position of a token in an HTML element - it just assumes that $sanitize is good enough, which it might well not be. Someone nefarious can manually enter
tags in the $text argument for an email template, for example, and those would be unaffected since they are not in tokens. Someone innocent could put the (XSS filtered) [site-slogan] in [site:name] and be vulnerable because XSS filtering is not the way to sanitize HTML attributes. Or they might put an Html escaped string into a title attribute, and see escaped HTML tags which ought really to have been stripped.Proposed resolution
hook_tokens() / hook_tokens_alter()- Remove
$options['sanitize'] - Always return either a plain text string, or a SafeString
- Remove
$options['sanitize'] - Always return SafeString as-is, or escape raw strings
- Remove
$options['sanitize'] - Always treat the
$textargument as HTML. This means if the user is expected to enter plain text in a textarea, the developer would need to doToken::replace(Html::escape($text)); - The return is an HTML string with tokens replaced. It’s then up to the caller to convert that to plain text with
PlainTextOutput::renderFromHtml(), or toXss::filter(), or to treat the entire string as safe.
- When the
$textargument toToken::replace()is Html::escape() (i.e. expected to be entered as plain text with no HTML tags), this works fine. - When the return value is converted back to plain text, this works fine.
- When the $text argument may contain HTML tags from user input, then the caller is put in a position where either valid token replacements (such as node:body) might have ‘safe’ HTML stripped by
Xss:filter(), vs. the potential of an XSS vulnerability. This is no different from 7.x, and we will not attempt to fix the bug in this issue. It is possible in a follow-up that we might be able to contextually escape/xss filter the token replacements when we have them in the context of the string.
Developers can use Token::scan() and Token::generate() directly if they don't like the above.
- (critical) Token::generate() returns markup for every token and make hook_tokens() return plain text (to be Html::escape()'d) or SafeString (Remove sanitize option from Token::replace()).
- Keep Token::replace() as not escaping $text argument and returning $output as a bare string (not a SafeString) with markup. This then requires:
- (critical) Check core token usages and make sure the $text argument is Html::escaped() if the user input is treated as plain text (as it should be for file directories, user e-mails)
- (major) Documentation issue that the first argument to Token::replace() is always treated as HTML. This means that user-entered strings that are not expected to have markup should be escaped before being passed in. User-entered strings with markup should mean the return of Token::replace() is Xss::filter()d or explicitly trusted (i.e. marked as safe) and permission-restricted.
Remaining tasks
User interface changes
None.API changes
Remove$options['sanitize'] from Token API, pass responsibility for sanitization to the caller.
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #190 | 2567257-178.patch | 64.55 KB | dawehner |
| #190 | interdiff.txt | 2.72 KB | dawehner |
| #182 | interdiff.txt | 3.09 KB | dawehner |
| #180 | interdiff.txt | 5.14 KB | dawehner |
| #180 | 2567257-178.patch | 64.55 KB | dawehner |
Comments
Comment #2
pwolanin commented"suitable for HTML" means always sanitized?
Comment #3
catchI think it means always sanitised and we should standardise on escape rather than filter so it's clear what you get.
Except for formatted text where we neither escape nor filter because we assume the result of the text format is safe but no change there.
Comment #4
catchI think we can switch to always escaping here, then in a non-API breaking follow-up we could add an option which lets you do escaping vs. filtering or whatever's needed for specific contexts.
However there's an outlier there which is text formats - since those should only ever be run through the text format - never escaped nor filtered, nor un-formatted - although unformatted is what HEAD and 7.x both do.
Comment #5
stefan.r commentedPosting a patch that breaks everything, but doesn't switch to escaping yet
Comment #8
catchJust noting - we can't do that if we always Html::escape() as opposed to returning unsanitized, so there's no bc way to extend this later if we go this direction.
Test fails just look like tests that need updating, one advantage of token API barely being used in core I guess.
Comment #9
mr.baileysComment #10
mr.baileys* Patch needed a re-roll.
* updated unit tests which were still explicitly testing for unsanitized token values.
Comment #11
xano@mr.baileys, @stefan.r, @pwolanin, and I discussed this issue extensively during the DrupalCon Barcelona sprints.
I believe this line is an excellent example of why this change is difficult.
$link_titlecontains text that is unsanitized (the original title string minus the token tags), and text that is sanitized (the token replacement values). Because this is one single string, we must either leave the unsanitized parts unsanitized, or sanitize the entire string, causing the token replacement values to be sanitized twice.My suggestion is to explore whether we can remove sanitizing from the token API and move the responsibility for doing this to the calling code. We have many more safety nets for this in D8 than we ever had in D7, so it's a potentially manageable approach.
Comment #12
catchI think removing the sanitisation could also work fine. Part if the problem we have here is lack of context which at least calling code has.
Comment #13
xanoAlright, taking a stab at that approach.
Comment #14
xanoComment #15
xanoAnother reason why this is a security improvement, is that the more context we pass on to our filtering/escaping code (the longer the strings are, ish), the better it can filter/escape security hazards.
Comment #16
lauriiiSome extra test coverage for this probably wouldn't hurt..
Missing line change
This is getting big, maybe change it for multi line syntax
Can you explain why this escaping method was chosen?
UrlHelper::stripDangerousProtocols()
Comment #18
stefan.r commentedComment #19
stefan.r commentedComment #22
xanoWhy did you remove this documentation?
Comment #23
catchWe discussedtl this issue in a Barcelona hard problems meeting and decided:
1. We should remove $sanitize and return things 'suitable for an Html context'. This can then be sanitised/formatted correctly for that context.
2. Doing that should be release blocking since it's changing the default return value generally from something escaped to not escaped.
3. We could use a good change notice for this issue.
Comment #24
effulgentsia commented+1
Huh? You mean the other way, right?
Maybe it should be regardless, but I think it's less clear cut if what we end up returning is always safe for HTML usage.
Comment #25
dawehnerIt will be in the format which can be accepted by the output strategies.
Comment #26
pwolanin commentedLooking at the test fails
Comment #27
pwolanin commentedFixing one token output and the test. Since we want the process node body or summary, the test needs to have that as the expected value.
Need to check other tokens in the patch.
Comment #28
xanoComment #29
dawehnerSome notes:
* hook_tokens() should document what the result should be
* MessageAction doesn't need use Drupal\Core\Render\RendererInterface; or use Drupal\Component\Utility\Xss; anymore
* TestExample has also an unused used statement
* Token::replace() should have documentation what the result is (the HTML representation and other particular bit if it constructs HTML should escape it properly)
Comment #30
stefan.r commentedWe'll want to do #27 for comments and taxonomy terms as well. Checked with @amateescu and ->processed will use the filter that is defined for the body field which seems like the right thing to do here.
Comment #31
pwolanin commentedok, working on those now.
Comment #32
pwolanin commentedComment #33
effulgentsia commentedI'm confused what makes the following (and most of the other changes in the patch) suitable for HTML context:
Or is the idea that something else (e.g., Token::replace()) will then convert all these values to the same thing that SafeMarkup::format() does for "@" placeholders?
Comment #34
pwolanin commented@effulgentsia - the idea is to get a consistent return value and then escape/filter where needed.
Comment #35
effulgentsia commentedRe #34, sure, but seems to me that "where needed" is before Token::replace() is done. Otherwise, what Token::replace() returns isn't suitable for HTML, and a caller would have no way to make it suitable for HTML. E.g., how would the caller be able to make
"[node:title] [node:body]"suitable for HTML if Token::replace() doesn't?Comment #36
effulgentsia commentedBut I kind of like the idea of making Token::replace() itself Html::escape() all values that aren't SafeString already prior to doing the str_replace(). Would there be anything wrong with that?
Comment #37
dawehnerShouldn't replace take care of that? The result should be proper HTML.
We actually need it, as otherwise we easily end up with escaping, don't we?
Comment #38
dawehnerThe assignment confuses me
Comment #39
pwolanin commented@effulgentsia - that sounds like the complexity we wanted to avoid. We want to rely on autoescape or otherwise avoid early escaping.
if I got a node body (which is HTML) I don't want it escaped.
Comment #40
dawehnerSo let's get back to an example of actual usecases of token:
so how do we ensure that the output will be safe at some point? Escaping outside would not be right.
Comment #41
dawehnerThis is what alex and discussed.
Comment #42
googletorp commentedOverall looks good, found a few things.
We should not add a TODO without a link to an issue (so we know this will actually be done)
Why stop using randomMachineName? Is this change really needed?
Comment #43
effulgentsia commentedTo illustrate #35, I tried the following:
First installed HEAD and:
test_mail_collectorby going to /admin/config/development/configuration/single/export, exporting the config for "Simple configuration" => "system.mail", copying it to my clipboard, going to the Import tab, reselecting "Simple configuration" => "system.mail", pasting what I had copied, changing "php_mail" to "test_mail_collector", and clicking the "Import" button.action_test.modulewith the following code:Use <em> tags instead of <i> tagsand a body of<p><strong>Strong</strong> and <em>emphasized</em>.</p>(I generated the latter by just typing "Strong and emphasized." into the CKEditor and then bolding "Strong" and italicizing "emphasized".){key_value}table forcollection=stateandname=system.test_mail_collector. The entry contains a bunch of properties, but the relevant one for this issue is:MailFormatHelper::htmlToText().Next I applied #41 and:
system.test_mail_collectorstate entry and found:I still think that #36 would be the approach that would actually do what the proposed resolution in the issue summary says:
"Suitable for HTML" means [node:title] needs to get escaped and [node:body] does not. Which if we make the
->processedproperty of formatted text fields return a SafeString and if we make Token::replace() run the same logic that SafeMarkup::format() runs on @ placeholders, would be what we'd get.Comment #44
pwolanin commented@effulgentsia - it's not at all clear to me why [node:title] should be escaped and [node:body] not?
Comment #45
catchI think this is @effulgentsia's point, and if it is, I agree with it:
node:title is stored as plain text, and needs to be run through Html::escape() before it can rendered successfully as HTML.
node:body is the result of a text format, which already returns HTML.
So if we return things 'suitable for HTML', then we need to HTML escape plain text strings like entity titles (i.e. not return as is, and not Xss:filter() them).
Comment #46
effulgentsia commentedRe #44, because
Node::baseFieldDefinitions()defines 'title' as being of type 'string' andfield.storage.node.body.ymldefines 'body' as being of type 'text_with_summary'.And Drupal\Core\Field\Plugin\Field\FieldType\StringItem has these annotations:
And Drupal\text\Plugin\Field\FieldType\TextWithSummaryItem has these annotations:
Which to me means that string fields (such as node title) need to be interpreted as plain-text while text_with_summary fields (such as node body) need to be interpreted based on whatever their format is, which means their
->processedproperty needs to be treated as HTML.#82751: Allow some HTML tags in node titles is an issue for allowing node titles to have HTML, but IMO, any patch that does that would need to either change the 'title' field from type string to one of the 'text*' types, or would need to redefine what type 'string' means.
Comment #47
stefan.r commented#45 and #46 seem in line with what me and @Xano had in mind here. We want this to return HTML - without any special sanitization applied to it (either escaping or filtering) -- except if we're dealing with plain text, in which case we convert to HTML by escaping.
#42.2 is not technically needed but we don't gain anything either by using random names everywhere, other than potentially chasing down random fails.
Comment #48
stefan.r commentedSo for all the fields we need to figure out their type, HTML escape anyway if they're to be interpreted as plain text and not do any sanitization if they're to be interpreted as HTML.
Addressing some previous feedback in the mean time,
Comment #49
effulgentsia commentedJust to make my suggestion in #36 concrete, this includes the change to Token::replace() that I'm recommending. This also reverts all the test changes, since with this approach, those will all need to be re-evaluated.
With this approach, Twig's
{{ foo }}, t()'s@foo, and Token::replace()'s[foo]all behave the same way.I think this patch mostly covers it. Text fields' ->processed is already a SafeString, so that covers node body and term descriptions. Most everything else should be escaped, which is what happens if nothing says otherwise. Possibly the only things not yet covered here are configuration values that we want to treat as HTML, such as site slogan and vocabulary description. Those we probably want the corresponding hook_tokens() implementation to Xss:filterAdmin() and then wrap in a SafeString. Or put them in a #markup element and drupal_render() that element, which amounts to the same thing.
Comment #52
chx commentedI do not see Dave Reid in this issue. That's worrisome.
Comment #53
dawehner@chx Informed him on twitter, maybe that works better than components on drupal.org, which IMHO don't help at all for that regard of information
Expanded the test coverage, to document what exactly should happen for various cases. The tricky thing is certainly, we here something like t() but with user input.
Comment #56
googletorp commentedStill missing to address comment about @todo from #42.
Comment #57
stefan.r commented@googletorp I think #49 accidently threw away my changes in #48 and rolled the new patch against an older version - will reapply the interdiff
Comment #58
stefan.r commentedHmm actually this is a completely different approach to what was discussed previously. Not touching this for now.
Comment #59
stefan.r commentedI don't really see this... these all do a safety check and otherwise escape now but isn't that's where the similarity ends as these all have widely differing use cases?
It would be good to see a list of pros and cons of both approaches - this last one where we santize the placeholders separately feels like an expansion of scope but does seem worth exploring.
Comment #60
pwolanin commentedIt feels like we should have a better way to Xss filter and mark a string safe than this:
The patch is injecting the renderer and adding another dependency but we are actually not rendering at all?
Comment #61
xanoWhile I understand @effulgentsia's concerns, I do want to add that any string that contains HTML can be perfectly acceptable plain text, as long as it's used outside an HTML context. If we escape node titles in
node_tokens(), for instance, we also run a high risk of accidentally escaping it again later on in the process.Comment #62
dawehnerI have a response to that: #2573233: Add a proxy variant of the renderer
Well, its the way how I think we should do it. It promotes the primary way of how we deal with things: render API.
Comment #63
pwolanin commentedHere are a couple test fixes, working on the last one. NR just for the bot
Comment #64
pwolanin commentedlast test fix
Comment #65
berdirLooks like you have a bunch of accidental reverts in the patch?
Comment #68
dave reidI'm deeply, deeply concerned about this change and its affect on contrib. For history, the Token module used to literally provide different tokens like [node:title-raw] before we could use $options['sanitize'], but it becomes much harder for end users to manage all those tokens, and know which one to use when. While I agree that it would be nice if we could provide context like mail vs plain text vs html.
My concern is that modules like Pathauto will exclusively have to rely on un-HTMLing and un-escaping every single token, and we've found that the process is not reliable 100% of the time. The issue summary here does not give contrib its due process for how to handle this change besides basically "sorry, you no longer can do this even though you could in Drupal 7." Using MailFormatHelper::htmlToText() is just not an option in a lot of those cases.
Comment #69
dave reidWhy couldn't this code conditionally run here if $options['sanitize'] is TRUE? I like that we're simplifying all the code in the individual hook_tokens() implementations, but I do not like that this is forced on every use of Token::replace().
Comment #70
dave reidLet's just change this to
Comment #71
catch@Dave Reid The $sanitize option just does not make sense as it is. The token could be used as part of an HTML fragment, HTML attribute, select list option, and those need different types of sanitization. Or while not sanitization, it might need formatting for an e-mail subject, log message, e-mail body etc. Right now when you pass $sanitize = FALSE on a formatted text area you don't even get text formats run which is completely unusable in any context (except possibly writing back to the database, hopefully no-one's doing that).
With t() we've made the decision to always accept/return HTML, then add #2509218: Ensure that SafeString objects can be used in non-HTML contexts to translate HTML to plain text (or other formats like select options if they need special handling). Why can't Path Auto use that API?
If something is not 'sanitized', but still contains some HTML (which anything run through Xss::filter()/filterAdmin() certainly can), then wouldn't PathAuto need to strip tags etc. anyway?
Also see the meta issue, and the sub-issues of that meta where this is discussed in much more depth #2506427: [meta] !placeholder causes strings to be escaped and makes the sanitization API harder to understand. Without that context this change may not look correct, but this overall approach (HTML in, HTML out, translate if you don't want HTML) is the product of dozens of hours of research and discussion
Comment #72
dave reidI still propose #70 as the way for us to not break things completely for contrib, seeing as we are close to RC1, and still make this change for most people. I'm just still *extremely* concerned that this is a very short-sighted change without much concern for contrib, which makes *extensive* use of this API, much more than core does.
Comment #73
dave reidIf #2509218: Ensure that SafeString objects can be used in non-HTML contexts were available, I would prefer if we could select an output strategy directly in the arguments to Token::replace() as an alternative to having to manually unescape everything, which is also the DX regression caused by this change.
Comment #74
dawehnerThis is a good point, but that particular developer experience improvement is not critical for itself, but we can improve that in a major task, still, given that it will be an optional argument.
Do you think this is okay?
Comment #75
effulgentsia commented@Dave Reid: I looked at what pathauto currently does, both the 7.x-1.x branch in https://www.drupal.org/project/pathauto and the current 8.x port in https://github.com/md-systems/pathauto. In addition to passing
'sanitize' => FALSE, they both do this to the return value:That there's that line of code in both the 7.x and 8.x versions is already an indication that
'sanitize' => FALSEis not working as pathauto wants it to work with respect to getting back something that's not HTML. You could not pass'sanitize' => FALSEand still end up with the same thing if you run the above on the output.Furthermore, by decoding and then stripping, you're also losing "HTML" that was entered into plain text and escaped. See more about that in #43, but basically I tried a node title of
Use <em> tags instead of <i> tags, and again, regardless of the 'sanitize' parameter, by pathauto decoding and then stripping, it ends up with a URL alias ofuse-tags-instead-tags, which doesn't contain the most important meaning of the node title.Instead, in #2509218: Ensure that SafeString objects can be used in non-HTML contexts, we have a
PlainTextSimpleOutputstrategy that strips tags, then decodes, so if pathauto uses Token::replace() with no 'sanitize' parameter, and then calls:It would end up with a URL alias of
use-em-tags-instead-i-tags(due to the removal of punctuation characters that PathautoManager::cleanString() also does), which I think would be better.I don't think that in pathauto's case, having to call
PlainTextSimpleOutput:: renderFromHtml($string)instead ofstrip_tags(Html::decodeEntities($string))is a DX regression. But, adding a $render_strategy argument to Token::replace() to make the DX better also seems like a proposal worth considering.Comment #76
pwolanin commentedadding a $render_strategy argument to Token::replace() is not on the short path to victory here. Let's not add more complexity.
I think we have a consensus plan and should get this fixed as planned.
Comment #77
effulgentsia commentedFor a clean patch review and git history, this should be committed separately. I opened #2573657: comment_tokens() filters instead of escapes some plain-text strings for that.
Comment #78
dawehnerSome small test additions.
Comment #79
pwolanin commented@effulgentsia - I don't think it makes sense to commit separately. We can can specify that on any backport issue?
Comment #82
effulgentsia commentedI think it's important to commit separately the parts that actually change the output of what callers that currently call Token::replace() without a
'sanitize' => FALSEwould get for any core token. Then the rest of this patch is just a removal of'sanitize' => FALSEand refactor of the Html::escape() responsibility from the hook_tokens() implementation to Token::replace() itself. #2573657: comment_tokens() filters instead of escapes some plain-text strings is the only such output change in this patch, so I'd like committed separately.There's also the following that I think we should revert in this patch:
I don't think this patch should change site slogan and vocabulary description from HTML to plain-text, which is what the above lines do. These are stored in configuration rather than content entity fields, so we can't use #46 to judge that, but these are configuration values that have traditionally been interpreted as HTML. And for example, SystemBrandingBlock::build() still does.
So I think for these, we need to go back to Xss::filter(Admin?)(), but then turn them into a SafeString as well so Token::replace() then knows to not escape them.
Comment #83
stefan.r commentedreroll of 78
Comment #84
stefan.r commented@effulgentsia I have added those 2 cases. Haven't reviewed the rest yet, so do we have any other ones still?
Comment #89
effulgentsia commentedFixes tests.
Comment #90
effulgentsia commentedHere's a patch with my suggestion for how to resolve these @todos.
Comment #91
effulgentsia commentedAnd here's some docs that explain #90.
Comment #92
effulgentsia commentedThere's an interesting consequence of this that might need discussion. Say that an administrative user puts some fancy tag (e.g.,
<table>) into the site slogan. Then a non-administrative user creates a tokenized string such as[site:name] [site:slogan]. Then calling code following the above recommendation will filter out the site slogan's<table>tag from the resulting string. I don't know if that's good or bad, and if we decide it's bad, how it can be solved.Comment #93
effulgentsia commentedHm, thinking about it more, I think #92 is a real problem.
Consider a token string with
[node:body]in it on a site that also uses Media module or similar:<img>tags would get stripped by Xss::filter() running on the output. It's certainly not desirable to strip images from a rendered node body just because the person who entered the token string isn't allowed to insert his or her own<img>tags into it.<iframe>tags. And Xss::filterAdmin() strips those, so having to run Token::replace()'d output through a '#markup' means losing videos, etc. from a rendered node body.So, I think this means that Token::replace() should return a SafeString object. But this requires trusting the $text argument itself, which usually comes from user input. We can Xss::filterAdmin() that input, but that would mean that a token string of
<a href="[comment:homepage]">Foo</a>would always get converted to<a href="homepage]">Foo</a>, because[commentis not an allowed URL protocol. Which is definitely preferable to the token name not being stripped, because otherwise<a href="[comment:title]">Foo</a>could be used as a way of bypassing URL filtering (since there's no reason to expect comment_tokens() to treat 'title' as a URL). So, as long as:<[fences:tag-name]>Foo</[fences:tag-name]>, or anything else that Xss::filterAdmin() would filter out prior to those being replaced.Then I think Token::replace() can just Xss::filterAdmin() $text and then return a SafeString, which would allow the [node:body] examples above to work properly, but still be safe in all cases.
And if callers want to sanitize $text more restrictively than Xss::filterAdmin(), then they can always call Xss::filter() themselves on it prior to passing it into Token::replace().
Thoughts?
Comment #94
effulgentsia commentedPotentially, we can support #93.2 and #93.3 with a $option for whether to Xss::filterAdmin() $text or $output? By default the former, but for modules that want to do either 93.2 or 93.3, they could ask for the latter?
Comment #95
effulgentsia commentedUgh. No. That approach could still leave us with the following:
Which means, if we need a solution to #92 / the [node:body] case of #93, then we need some other way of escaping or filtering $text, such as:
Comment #96
effulgentsia commentedOk, so after the last 4 comments, here's my current recommendation:
As part of this issue, change #91 to implement #95.2, which means:
Then, have a non-critical follow-up to add a $options['filter_mode'] (or better name) which could be set to Token::POST_FILTER, which would thus instruct Token::replace() to not filter $text, and instead Xss::filterAdmin() the output. This would then address the rare use cases like #93.2 and #93.3.
Maybe have another non-critical follow-up to allow $options['filter_mode'] to be set to Token::NO_FILTER, which would instruct Token::replace() to neither filter $text nor the output. And consequently, just return a bare string, not a SafeString. This would allow for exotic callers that both want tokens in attributes and simultaneously want IFRAMEs within node bodies retained. But the caller would then need to know how to handle the output string. For example, the caller could turn the string into a SafeString, but would need to document what steps it has taken to ensure that it's truly safe.
I'm tempted to set to "needs work" for the bullet points above, but leaving at "needs review" since so far this is only my recommendation, so could use more consensus before implementing.
Comment #97
stefan.r commentedi don't know that we need to be supporting tokens in attributes - we don't in SafeMarkup::format() either.
This all sounds pretty complicated by the way. This is a hard problem (similar to translation except also with untrusted user input outside of the tokens) but isn't there an easier possible solution?
Comment #98
stefan.r commentedI don't think we can trust $text... which makes this all pretty hard.
Maybe we can whitelist the specific iframe/whatever tag we're looking to insert somehow (and skip sanitizing that)
Comment #99
dave reidThis is the problem with trying to make this change all at once. With 100% certainty I've used tokens used in the same blob of text used both as part of an attribute, an attribute itself, or HTML. You cannot simply know ahead of time how the user is using tokens, which is the entire reason the token API exists.
Comment #100
stefan.r commented@Dave Reid I think what we mostly want is a predictable result from Token::replace() - in talks at Drupalcon BCN it was decided we'd use HTML everywhere for transporting strings, and then convert to plain text or attribute-safe values as needed (or other contexts). We assume input and output are markup in t() as well.
The problem is this "translation" is even harder than t() because the input is untrusted. The sanitize parameter seems problematic. If you have a better solution it'd be good to hear.
We don't necessarily have to cater to all use cases for D8 in the same string though. If truly it's all of those, maybe they could see about using other options rather than token API?
Comment #101
chx commentedSTOP
STOP
Take a deep breath, write a new issue summary (or an issue summary) because the current one and the whole issue is completely off the rails and confusing and needs to be re-focused.
We need to do a requirements analysis: what is the purpose of this API? What are the inputs? What are the output contexts? Is one API function doing too much? Should we split? How much existing functionality can we reuse?
Go!
Comment #102
stefan.r commented@effulgentsia I think the previous approach was to have this applied on the full text for all cases after having used the unsanitized HTML values everywhere - could you explain what the advantages of the current approach are? Maybe if something needs to be /not/ XSS filtered we could somehow white-list it?
In any case @chx made very good point in #101, let's start by addressing that?
Comment #103
googletorp commentedLooks like we need to work more on this, also we still have a @todo with no issue related to it. Todo's should always have a followup issue or we should fix it in this patch.
Comment #104
chx commentedComment #105
lauriiiComment #106
catchI think there was an in person discussion about this yesterday. I wasn't at that one so this might be out of date.
In reply to the last few comments, for me this issue is only rc blocking due to the Api change we have to make to hook_tokens().
Adding or deprecating things from token replacement itself all feels like followup. Either hardening or dx improvements but all smaller changes that can be done with only small bc breaks or none at all.
Comment #107
chx commentedWe had a big discussion with dawehner and catch and the simple decision is this.
#75 shows the main customer of the token API, pathauto already uses
strip_tags. Let's do this. If you want to produce HTML use Twig. If you want plain text, you can use tokens.Comment #108
chx commentedComment #109
pwolanin commentedSo the plan is to use strip_tags() and/or Xss::filter() on all token output? Are we going to escape any of them?
Comment #110
catchWe should use the plain text formatter on Html. That either means hook_tokens() always returns plain text and is responsible for transforming Html to it. Or we return Html and convert to plain text centrally. Or we return plain text/safe string and convert the safe strings.
Comment #111
dave reidSorry, but tokens need to support HTML by default. There are *too many* use cases in contrib that would just simply break moving to D8. I would rather it return HTML by default (and not really care about using tokens in attributes, should be a trusted user that is allowed to do something like that), and we can add DX later for modules like Pathauto that need plain text versions only.
Comment #112
catch@Dave Reid can you link to some contrib modules that require this?
Comment #113
berdirI'm not sure if we are talking about the token values being forced to plain text or the whole resulting output.
For the token values, I'm not sure. But the whole output isn't an option, to give two examples:
* Using HTML for the user mails (e.g. new account, password recovery). While very ugly to edit, it was always possible to add HTML in there and send HTML mails (using contrib modules)
* In simplenews, I'm using token replacements and I'm doing that *after* a node has been built and rendered. I might send the same mail to 100k recipients, with the only difference between the token values (e.g. Hi [firstname] [lastname]). I do not want to rebuild the node for every recipient. I've discussed with @dawehner that it might be a possibility to use an inline twig template with the rendered output as template, not sure yet.
Comment #114
catchSo what I think we can do:
- hook_tokens() returns either a plain text string, or HTML - the HTML is identified via SafeStringInterface - same as many other places like Attribute now.
- for plain text uses, the safe-markup is converted via the plain text formatter, and the plain text is left as is.
- for HTML uses, the plain text is run through Html::escape() and the SafeMarkup() is left as is.
Whether we support HTML output in core I still think is up for discussion. It's quite possible to replace the Token service and add that capability back from contrib.
Comment #115
catchHere's what it looks like if:
- hook_tokens() returns plain text or HTML-as-SafeString
- Token::replace() only handles plain text.
I think it's reasonable to either:
- put HTML-support back into Token::replace() (or even a separate method) in a follow-up
- leave that support to contrib
What I don't think we should do is leave Token::replace() handling HTML without being able to properly contextually escape - i.e. the issue effulgentsia found. Better to not have it, than to have it and wrong.
Patch is untested.
Comment #117
catchTaking a look at those fails.
Comment #121
googletorp commentedWe still need to either fix this, or make a follow up issue which solves this, and link to it in the comment
Comment #122
catchLess fails, not zero yet.
Comment #125
dawehnerLooking into in right now
Comment #126
dawehnerWorked on an idea I had, let's
Token::replace()return aTokenStringobject. This object escapesthe string by default. For some usecases, for which we need HTML, it though has an additional method
::getXssFilteredRawString()which returns as xss filtered result of the token replacement result.
Comment #129
dawehnerWay less test failures for now.
Comment #132
dawehnerThis has at least a failure in some link test due to some weird UTF8 weirdness.
Comment #133
dawehnerThis was a nice drupalcon!
Comment #136
catchMore solution-agnostic title.
Comment #137
dawehnerRemaining issue: #2568045: Make it impossible to double escape with #plain_text or #2575615: Introduce HtmlEscapedText and remove SafeMarkup::setMultiple() and SafeMarkup::getAll() and remove the static safeStrings list which would solve things
Comment #140
oriol_e9g$string = Html::escape($string)should be$string = Html::escape($string);Comment #141
k4v commentedLet's fix it :).
Comment #142
stefan.r commentedComment #143
geertvd commentedComment #146
catchBig issue summary update after a hangout.
The existing patches already do more or less what we decided on the call for hook_tokens() and Token::generate().
What we've been missing is consensus on how to handle Token::replace().
There is not a perfect solution, but I think we found a way to make things work properly for two out of three use cases and not make the third any worse than it already is.
Comment #147
dawehnerComment #148
wim leersI think this change makes sense overall, but can you explain why it's necessary to do this here?
(And why it's only necessary to whitelist
<p>and not also<br>?)"entered" is wrong here, because it implies "entered by the user", i.e. "user input". But it's really just $text with tokens replaced.
s/its/it is/.
Fixed.
I think: s/suggested/recommended/ ?
s/twig/Twig/
Fixed.
"xss admin filtered" is not very understandable.
Fixed.
The comment seems to not really match the logic: if it already is a string that is known to be safe, then nothing happens. So it's totally possible that markup that is known to be safe remains markup, and is not "formatted for plain text".
This comment makes a lot more sense; I think we want something similar in the previous point?
Interesting! I wonder if we shouldn't update
RendererInterface's docs instead?Incomplete docblock.
Fixed.
I'm pretty sure this means it's relying on the automatic filtering, which means this indeed won't be escaped.
But "fully escaped" and "the main rendering process" are rather vague terms.
This is just a way to let the
Rendererdo filtering for you, this could just as well do:Would (something like) that perhaps be actually better? Right now, it feels like the Render system is only used by accident.
Two unused
usestatements here now.Fixed.
Aren't these
Html::escape()calls wrong too? I suspect the only reason these still exist is because they didn't happen to cause any test failures.EDIT: nope, I was wrong, the auto-escaping behavior in the token service is what does this.
These changes seem unrelated?
One unused
usestatement because of the token hook changes here.Fixed.
Shouldn't we document why?
One unused
usestatement because of the token hook changes here.Fixed.
Debug statement that can be removed :)
Fixed.
Unused.
Fixed.
Two unused
usestatements because of the token hook changes here.Fixed.
Similar remark as earlier.
Two unused
usestatement because of the test changes here.Fixed.
Two unused
usestatements because of the token hook changes here.Fixed.
And again.
Dead code.
Fixed.
One unused
usestatement because of the token hook changes here.Fixed.
One unused
usestatement because of the token hook changes here.Fixed.
Leftover debug statement.
Fixed.
"the tokenize"?
Debug leftovers.
Fixed.
One unused
usestatement because of the token hook changes here.Fixed.
Unused.
Fixed.
Unused and non-existent.
Fixed.
Comment #149
stefan.r commentedWhere specifically do we need the p? I don't see any Xss::filter() in the patch anymore?
This could be clearer.. "expected input was"?
s/it/the result/
Not in scope here, but maybe we codify on the interface that the result must be XSS filtered on the interface so we can just type hint the interface instead?
Is this needed? In HTML contexts we expect the caller to XSS filter, right?
I guess the processed values return a SafeStringInterface object, so they wouldn't be auto-escaped?
Don't know if these changes are still needed?
I don't think there's anything to fix here anymore?
s/tokenize/tokenization? Also why do we need this to be a string?
Comment #150
dawehnerThank you @Wim Leers and @stefan.r
Feels similar for me, fixed!
Well, I think its pointless to typehint against a specific implementation. There are way more areas in core you would have to worry about if the renderer doesn't work as you expect it to be.
Indeed this is much better IMHO
Yeah I think so.
Now that we use the
PlainTextOutputthis is no longer true.At some point we might want a XssFilteredMarkup object.
Well, but just in case the output calls xss::filter, which might not be the case when
$textis not user input.I guess the processed values return a SafeStringInterface object, so they wouldn't be auto-escaped?
Yes, exactly, see
\Drupal\text\TextProcessed::getValueComment #152
dave reidI would say this should happen *before* the 'callback' is executed above. Pathauto needs to call PlainTextOutput::renderFromHtml on each individual token, but would need to do so after this sanitization happens.
This seems suspect for me. I don't think the HTML link should be escaped by default. Should this be a safe string?
Why is this not returning an Xss filtered string with SafeString?
I could see this one not wanting to be escaped by default. Should this be using SafeString? Also why would we need to sanitize an email address?
Comment #153
stefan.r commentedShould it really? If callbacks do their own sanitization they can mark the string as safe to skip autoescaping in Token::replace()...
Could Pathauto not do this on the text with the tokens replaced rather than on the individual tokens? I don't know that we should want to have any use case that combines plain text and HTML? When outputting the string we're either in an HTML context or in a plain text context right?
That's what #markup does, I think SafeString is @internal and not supposed to be used in modules like taxonomy?
The idea was to use #markup + renderPlain() everywhere to create XSS filtered safe strings, just to set the right example but I agree it's a bit silly, we might want a helper method that does this as well.
I think the Html::escape() is just used here to convert plain text (the email address - which might contain characters like &'"<>) into HTML?
Comment #154
effulgentsia commentedI agree with #153's questioning of this. But the issue summary says that the escaping of raw strings should happen in Token::generate(), which I agree with. So that would put it before the callback.
Comment #155
dave reidCallbacks should not be doing sanitization. That is for hook_tokens_alter().
As an API, the callback should be executed last before token replacement. I don't think we should be changing this behavior in Drupal 8.
We are literally using the same patten of using SafeString elsewhere with other token values here. I'm asking for consistency with what is being used with [site:slogan] which is
$replacements[$original] = SafeString::create(Xss::filterAdmin($slogan));. the taxonomy description should be doing the same.Conceded, looks like '&' is the only valid character allowed in email addresses, so that seems ok.
Comment #156
dave reidAlso, taxonomy term description can contain HTML, so it should not be rendered as plain.
Comment #157
stefan.r commentedHeh, $renderder->renderPlain is confusingly named... it does render HTML :)
Comment #158
catchWhat's the use case for sanitization in hook_tokens_alter()?
Comment #159
catchYes renderPlain() really means 'render outside of a request context'...
Comment #160
dawehnerAh so we should do things before the callbacks, fair.
Sure, let's not change that now.
What about introducing a XssFilteredMarkup much like the EscapedString? Otherwise I think we should standardize on using renderPlain() which renders a render array without taking care of #attachments
Comment #161
dawehnerAre you sure about that? drupal_attributes() already
escaped all the values.
Comment #162
dave reid@dawehner: Sorry, file_create_url() returns a string URL, not a link. I don't think it needs HTML escaping because it has been generated without user input.
Comment #163
stefan.r commentedSo per the issue summary Token::replace() will work similarly to t() in that both the input and output are expected to be HTML, i.e. when working with plain text, the input needs to be escaped to be converted to HTML and the output (the text with tokens replaced) will need to be converted from HTML using PlainTextOutput::renderFromHtml().
So even if autoescaping happens before the callback (fair enough), I don't think we should ever need to run PlainTextOutput::renderFromHtml() on individual tokens, given that PlainTextOutput is intended for converting full HTML strings, such as the output of Token::replace(), to plain text contexts, such as email? So for this to be consistent with what happens in t() and elsewhere in core, the text format conversion should rather not happen in the callback (even if it's currently documented that the callback *should* be used for that), but somewhere else (see EmailAction::execute()).
@dawehner I do think depending on the renderer and running $renderer->renderPlain(['#markup' => 'html string') is a bit bothersome, so something more convenient such as XssFilteredMarkup does make sense.
Comment #164
stefan.r commentedTo make this patch more easily reviewable, I wonder if we could put assert()'s or comments above the $replacements[$type] statements clarifying the type all the values (i.e. SafeString/URL/link/plain text string)
Comment #166
dawehnerAdded that to some pieces but I think
Comment #167
dawehner#2577827: Add a XssFilteredMarkup
Comment #168
dawehnerCatch also suggested to open up a follow up to review all core tokens, just to be sure.
Comment #169
kgoel commentedComment #172
plachLooks great!
What about "An HTML string..."?
Should we mention
SafeStringInterfacehere?Lovely :)
Just curious: wouldn't this be equivalent to the following code?
Is replacing this kind of code the goal of
XssFilteredMarkup? If so +1 :)Also, in that case can we avoid to inject the renderer since this is just temporary code?
Can we enclose these occurrences of "processed" in double quotes? The comment is hard to read otherwise.
Can we use
[]instead ofarray()since we are changing these lines?Can we use
FormattableStringand[]instead ofarray()since we are changing these lines?"If not", I'd say :)
Also, are we planning a follow-up or will this be addressed in this issue?
FormattableString?[übernitpick] Can we use
::tokenizeValue()to make the comment more readable? :)Comment #173
stefan.r commentedI think the comments in 8 can just be removed
Comment #174
dawehnerGood question, not sure actually. I mean we better refer to some potential summary documentation for our sanitization system.
It is certainly out of scope of this issue. Well, you know, its one of those todos, we could just remove and nobody will notice, in other words, someone put some thoughts in adding it.
You try hard :) Not sure whether you can trump the empty line at some point.
Comment #175
plachIf we happen to reroll this:
A couple of lingering
format_string:)Comment #176
plachAnd I meant this
Comment #177
wim leersMy main concern is the use of the
Rendererfor XSS filtering. I raised this in #148, @stefan.r raised it also in #163, @plach raised it in #172.4. I think those concerns will be addressed in #2577827: Add a XssFilteredMarkup, but if that's the case, I think it's important to add @todos to the relevant places.s/the usage of/using/
Observation: #2576533: Rename SafeStringInterface to MarkupInterface and move related classes will make this a lot clearer.
This is missing the docs for the first 3 params, which I added in #148.
Why does this use the renderer? I think a
@todo Fix in https://www.drupal.org/node/2577827may be necessary.I think we should fix this @todo here?
Why does this use the renderer? I think a
@todo Fix in https://www.drupal.org/node/2577827may be necessary.Why does this use the renderer? I think a
@todo Fix in https://www.drupal.org/node/2577827may be necessary.Comment #178
plachGood point, we could also avoid to inject the renderer, since that's temporary.
Comment #179
wim leers#178 My thoughts exactly!
Comment #180
dawehnerThank you for your review wim!
Sure, let's copy the things from the parent class.
I added a todo for that. Well, the situation did not got worse by this patch IMHO. Its the intended usecase of producing HTML, which means that things gets escaped, I don't see why anything needs special treatment for now.
Comment #181
dawehnerLet's just go with the proper way for now, given that the other issue hasn't been settled yet, like whether we want to deal with that or not.
Comment #182
dawehnerThis time with the interdiff of my filesystem
Comment #183
wim leersThanks!
Now we have this one twice. Can be fixed on commit.
Comment #187
alexpottwebchick, stefan.r and myself should also be credited since we all were present on a recent call that decided the approach to take.
Unused use
This needs to be SafeMarkup::isSafe() since the static safe list still exists (although is not used by core)
Nice, I hoped to see this!
Comment #188
wim leersRe-testing #180, it failed because the #174 patch actually got committed in the mean time, which meant the patch could not possibly apply. Since it was then reverted, a re-test now should work.
Comment #189
alexpottOkay I will do the fixes in #187 on commit
Comment #190
dawehnerHere is a new patch.
Comment #191
alexpottCommitted 6b4e81a and pushed to 8.0.x. Thanks!
Comment #193
dave reidI think we need a change record since this modifies the expectation of hook_tokens() implementors.
Also need to review tokens.api.php for changes as well.It's OK for the returned values to include HTML (not plain text strings), since they will be auto-escaped? Maybe it's my definition of 'plain text strings' doesn't seem to match everyone else's that works on Drupal 8? If its the latter, I find that very confusing.
Comment #194
dawehnerWorking on a CR. I'm really confused how this could have happened :)
Comment #195
catchNot sure what your definition of plain text is to compare with everyone else's. There's a few examples in this and other issues, but:
I like the <blink>tagis plain text.I like the <blink> tagis HTML. And is what you get after you put the plain text string through HTML::escape().If you use the plain text literally in an HTML document, you don't get what you actually wanted because it's effectively encoded incorrectly.
If you instead have an HTML string like:
I like the <strong><blink></strong> tagthen you need to return SafeString (soon to be Markup) to prevent that getting double escaped by Html::escape().Then PlainTextOutput::renderFromHTml() can take that HTML string, and give you back
I like the <blink> tagfor use in an attribute value or e-mail subject etc.When that HTML comes from user input, it has to be XSS filtered (or the result of a text format) or something that protects against XSS - unless you're just going to convert it to plain text again.
Comment #196
dave reidI guess my definition is based on what is *inside* the string, not on how it will be output. For example, I considered results of check_plain() a plain string (based on the pre-D8 function name, and that it no longer contains any HTML tags), and results of XSS filter function containing HTML. You're describing the opposite. Maybe I'm the only one confused by that.
Comment #197
stefan.r commented@Dave Reid yes, others (including Dave Rothstein himself) had the same confusion about escaped HTML vs plain text.
Currently all of Token::replace(), t() and format_string() have HTML markup as both input and output.
Maybe the planned rename from SafeString to Markup will make this all clearer... Markup objects can be converted to plain text contexts using PlainTextOutput if needed.
Comment #198
catch@Dave Reid, check_plain() has this function summary in 7.x:
Hasn't changed beyond a few characters since 4.6 https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/chec...
So it's very clear on the distinction, but many people see check_plain() and see it as 'check that a string is plain', not 'check a plain text string and encode any HTML characters in it'. Html::escape() is better at least.
You're not the only one confused by that, people have also been confused that filter_xss() doesn't sanitize strings for use in attribute values, and several other levels of confusion.
This is why contrib has had dozens if not hundreds of XSS SAs over the years, and why it's taken a long time to get Twig autoescape and the related APIs in shape in 8.x. The big change in 8.x is that rather than giving people a few HTML sanitization functions and expecting them to use them properly all over the place, we now handle nearly all of that for you in the rendering system.
This patch brings token into line with that.
The $sanitize option was never, ever in-line with core's sanitization API or any acceptable approach to safely outputting HTML on the internet at all. dww brought this up in the original token-in-core issue in #113614-38: Add centralized token/placeholder substitution to core then chx again in #113614-49: Add centralized token/placeholder substitution to core and it was not addressed until today almost seven years later.
Comment #199
dave reidI'm just confused about the terminology semantics, not needing to be lectured about the change.
Comment #200
catchWell it's not just terminological semantics it's a conceptual issue that's resulted in over a year of work trying to fix SafeMarkup (most of it not by me I should add, I wasn't paying attention properly to it until a few weeks ago)
i.e. when we think of an HTML string as a string with an HTML tag in it, vs. a string that is formatted for output as an HTML fragment, that makes this entire area much, much more complicated and error-prone.
So when we break everyone's hook_tokens() implementations (and !placeholder usage) and some of them read this issue, hopefully #198 is useful even if it's not for you.
Comment #201
alexpottThe patch in #190 didn't actaully contain the interdiff. Committing it. Committed 10626d8 and pushed to 8.0.x. Thanks!
Leaving at "needs work" for the CR.
Comment #203
plachThe CR is available at https://www.drupal.org/node/2578365. I just performed a couple of adjustments,
mainly I removed the renderer example since we are planning to introduce.XssFilteredMarkupand replaced it withFormattableStringEdit: that was even more confusing, I mentioned the
XssFilteredMarkupissue instead.Comment #204
webchickChange record exists, any improvements can be made there directly.
Back to 1. :)
Comment #205
effulgentsia commentedI'm very happy to see this committed. Here's a small follow-up: #2578569: Move token sanitization from Token::replace() to Token::generate()
Comment #209
plachBot, stop!
Comment #210
berdirPosting this here for now, will probably open a follow-up issue.
I've been working on updating token.module and adjusting functionality/tests for this. Which is probably something we should have done before committing this, to make sure that this works for more than the few use cases that core has.
I think there is at least one example there why supporting some sort of sanitize => FALSE is useful and important.
The basic use case is when you have user-provided, unsafe input and want it to be continue unsafe and un-escaped, because you then rely on autoescape.
One example in token.module is the block label, it has this code:
The problem is that now the block label tokens are escaped twice. There's a test that is creating a node with a ' in it, and right now, that is getting escaped twice (which is exactly what this code is testing), since we force-escape all token return values and then escape the whole string again.
I don't see a proper way to fix this right now. What technically works is using PlainTextOutput::renderFromHtml() but clearly it is not correct to use that in non-plaintext output.
We don't have to pass it to hook implementations, but I really think we need a flag to prevent auto-escaping. We even document:
The caller is responsible for choosing the right escaping / sanitizationbut don't actually allow to caller to do that, at least not for token values. But if the token input is untrusted and will be escaped later, we must treat token replacements as untrusted too or we are guaranteed to have double-escaping problems?Comment #211
effulgentsia commentedI don't see why it's clearly not correct. "The label is automatically escaped" means that your intended semantics for
$build['#configuration']['label']is that you want that value to be treated as plain-text. So I think PlainTextOutput::renderFromHtml() would be the correct thing here. Unless you want to support markup in the block's label (and have it interpreted by the browser as markup), which would then require you to figure out how to properly sanitize it, but that doesn't appear to be your use case, so I think PlainTextOutput::renderFromHtml() is still correct here until you decide you want to change your use case.Comment #212
catchFor those cases wouldn't taking the return value and marking it as Safestring work?
If you get back the raw string, you have no way to know if it should be xss filtered or escaped so I don't see it being useful. At least until we add value objects for UserInputHtml or similar.
Comment #213
alexpott@catch I think we'd have to escape the input too
Comment #214
berdirI'm not sure, but I see a difference between plain text and user input. The first is something that is used in a plain text context, like drush or a mail subject. The second is not-yet-escaped text that *will* be used in an HTML context.
I either need to run that on the complete return value, which might have an unexpected effect (not 100% sure) on the provided block label. What if the block label is, to use an existing example mentioned above:
I like the <strong><blink></strong> tag [some-token]. Is first calling PlainTextOutput::renderFromHtml() and then auto-escaping it again not going to change the output? (It's not going to work as expected anyway, but still). Alternatively, I could set that as the callback for each replacement, but that seems like a complicated API for an IMHO not so uncommon use case.No, I can definitely not do that, exactly because the original string is user-provided and *not* safe. Token::replace() does not escape that, just the token replacements. And that's exactly the problem, that I get a string back that was *partially* escaped. That's completely at odds with the whole SafeString concept. We either need fully escaped or safe strings that we can mark as such or unescaped user-input that will be auto-escaped, not a mix.
Comment #215
effulgentsia commentedRe #214, that gets back to the last part of #211. If you want block labels to support HTML, then maybe what you want is:
?
Or if we don't want to use a '#markup' array for that kind of use case, there's #2577827: Add a XssFilteredMarkup.
Comment #216
effulgentsia commentedOn the other hand, if you don't want block labels to support HTML, and you want to treat what the user types as completely literal text: i.e., if they enter:
I like the <strong><blink></strong> tag [some-token]and you want the pre-token part of that to show up as the literal text of the block label (i.e., what the site visitor sees as the label is exactly that, and not
I like the <blink> tag [some-token]with<blink>bolded), then you can do that with:Comment #217
berdirI don't want to Xss filter, core doesn't do that either and I don't want to change how block labels behave in token.module.
Yes, something like #216 would work but I am the only one to think that's a very complicated solution? We even document that Html::escape() and Html::decodeEntities() are not counterparts:
To me that sounds code like that is using it for something that it isn't intented to. If you use "é" for example, then the resulting block label will use "é" which is not something a user will expect.
Comment #218
catchI missed that the first argument was user input. So yes #216 is it.
@berdir it is complicated but that's because it's concatenating multiple sources of user input and converting between plain text and Html more than once. Which is complex. #216 is something we can add either a new method or an option to the token Api for. What it's definitely not doing is just 'sanitizing' - the original Api did not support this properly either.
renderFromHtml() does a strip tags as well as a decode entities, so yes it's completely not a counterpart to Html encoding - this is what core never had an Api for until a few days ago and what fixes the longstanding bug in pathauto that effulgentsia pointed out earlier in the issue.
Comment #219
berdirYes. But that plain-text/html conversion is IMHO completely self-inflicted, there is no good reason to force that and make life complicated for callers. Just adding that option makes it in my opinion very easy :)
That's basically what I'm asking for: #2580723: Fix token system confusion, with new function Token::replacePlain(). Has a first patch that makes token_block_view_alter() pass.
Comment #220
effulgentsia commentedComment #221
effulgentsia commented#220 was me attempting to add #2580723: Fix token system confusion, with new function Token::replacePlain() as a related issue. Trying again.