Problem/Motivation
Follow-up from #1856562-31: Convert "Subject" and "Message" into Message base fields.
1) An attacker can sent contact messages with arbitrary unfiltered HTML. This is a concern for security however it has been cleared by the security team for discussion in public, see #59.
To reproduce:
1. Enable the contact module
2. Enable the swiftmailer module to allow sending HTML mails
3. Use the mailsystem module to set swiftmailer as the default formatter and transport
4. Grant permission for anonymous users to use the site-wide contact form
5. As an anonymous user send a contact message containing arbitrary malicious HTML
The attacker could insert script tags even but there isn't much point as the mail client won't run scripts. The real threat is to insert disguised malicious links into a mail that the site admin is likely to trust because it comes from her own site (with all the correct headers to convince the mail client it's not spam).
For example the attacker could send a message that looked like ordinary junk mail. Then at the end it has a link "block future messages from this user". The site admin might think wow my developer added this great new feature yes I'll click that. The link leads to a fake site that looks like the log in page of their site, so they happily enter their password.
2) Contact messages from ordinary friendly users are corrupted if the message resembles HTML. For example
If a
Becomes corrupted to
If a**
Explanation
The contact_message entity renders to HTML markup exactly as expected like any other entity. The problem is that this HTML is decoded through MailFormatHelper::htmlToText() twice.
1) in \Drupal\contact\MessageViewBuilder::view()
2) in \Drupal\Core\Mail\Plugin\Mail\PhpMail::format()
Or in the case of using SwiftMailer, the HTML is converted to text in MessageViewBuilder::view() which gives the original unfiltered user input. However the object still implements MarkupInterface which is a clear breach of the terms of the interface
If there is any risk of the object's __toString() method returning user-entered data that has not been filtered first, it must not be used.
Swiftmailer sees that the object implements MarkupInterface so trusts it and displays the output without any escaping.
Proposed resolution
Remove the decoding from \Drupal\contact\MessageViewBuilder::view().
The documentation for MailInterface::format() refers to hook_mail() which states clearly that this is allowed
* - body: An array of lines containing the message to be sent. Drupal will
* format the correct line endings for you. MailManagerInterface->mail()
* sets this to an empty array when the hook is invoked. The array may
* contain either strings or objects implementing
* \Drupal\Component\Render\MarkupInterface.
Remaining tasks
- Maybe write text for release note??
- Raise a follow-on issue because PhpMail converts incorrectly: it runs
MailFormatHelper::htmlToText()even if the line is already a plain text string. This causes corruption of any message that resembles HTML (for example a user account mail containing a < character).
User interface changes
None.
API changes
None. The documentation for the API has been improved to clarify the responsibilities of the mail plug-in.
It is possible that a Contrib mail plug-in is missing the correct code to convert but has been getting away with it until now. In that case the plugin already exhibits this bug if used with popular modules such as swiftmailer, so this scenario is unlikely except for modules with very low numbers of sites using them. The consequences of this problem are that contact messages could be sent as plain-text but including markup - there is no security risk.
We should consider adding a warning to the release note.
| Comment | File | Size | Author |
|---|---|---|---|
| #99 | interdiff.txt | 574 bytes | andypost |
| #97 | contact-decode-D8.2223967-91.patch | 5.01 KB | adamps |
| #91 | contact-decode.2223967-interdiff-87-91.txt | 1.66 KB | adamps |
| #91 | contact-decode.2223967-91.patch | 5.01 KB | adamps |
Comments
Comment #1
andypostThis is existing bug since d7, so could be postponed til 8.1
Comment #2
andypostBut also can be a contrib blocker because there's no way to get original message in mail backend
Comment #3
berdirLooks like this changed but still worth fixing, it prevents to theme those mails as HTML. Not sure what exactly is going to happen with this, but this might help.
Needs manual testing of the output with and without a HTML mail formatter/sender.
Comment #4
berdirThat had some unrelated changes.
Comment #5
berdirOh, there is another call to checkPlain() missed that. but we also need to get rid of the double-conversion in the patch.
Comment #7
LKS90 commentedHere is a patch that also removes the checkPlain() call.
I can't find any other occurrence of the
MailFormatHelper::htmlToText()function, The @todo about improving htmlToText is probably still valid, just a little out of place without a single htmlToText() call in the class.The
MailFormatHelper::htmlToText()calls in the test can be removed (without the test failing), but I'd like some feedback about that first, don't want to post 20 different patches :D.Comment #8
juanse254 commentedTested locally as well, everything seems to work just fine.
Comment #9
andypostis that secure?
Comment #10
LKS90 commentedcheckPlain() will be gone soon anyway and #markup is filtered with Xss::adminTagList. If we want more restrictive filtering, you have to add
#allowed_tags = {TAGS GO HERE}.But I don't know which tags should be allowed in such a message?
Comment #12
berdirWhat we basically need/want to test is how this behaves in combination with the default plain text mail plugin and with one that supports HTML.
So install and configure https://github.com/webflo/drupal-swiftmailer, then send a contact mail and check how it looks. Is the HTML in there, is it escaped, ...
I'd guess this also needs a reroll
Comment #14
LKS90 commentedI think this is already been fixed in #2559605: Convert SafeMarkup::checkPlain() in render arrays to #plain_text. It uses the
'#plain_text'key instead of the'#markup'like it did before.The content-type is always plain-text (even though HTML might be selected). Is that the expected behaviour? I tested it without any patches, emails sent with the core and the swift mailer plugin (configured either as plain-text or HTML) all show up in the same way:
Comment #15
LKS90 commentedTested it with real emails. Yes, there is a new issue, the old issue seems resolved by the change in #2559605: Convert SafeMarkup::checkPlain() in render arrays to #plain_text but causes emails to be always sent as plain-text.
I sent the HTML email with the following content:
I received this:
Comment #16
berdirThe contact message body is one thing. That actually should be escaped because you *are* sending a plain text mail.
To send a HTML mail, you must set up something like swiftmailer. The HTML that I'd expect then is the one from the field templates for example.
In Simplenews, we have a test mail plugin and a test that simulates an HTML mail.
We should bring that into core and test a contact message, that just like SimplenewsSourceTest::testSendHTML() explicitly puts special characters like > and ' into mail subject and body and then make sure that it is printed as expected for both plain text (converted according to what we expect) and HTML (escaped exactly once for body). Subject is always plain.
Comment #17
LKS90 commentedAlright, I set up everything correctly this time, I think. The message is formatted now (at least the content I entered).
Source Content:
The email in my inbox:
The incorrect formatting for the unsubscribe link is an issue for simplenews I guess.
For the test-porting:
Extend testSendPersonalContactMessage() in ContactPersonalTest with a message that uses HTML formatting (which requires a test system like simplenews, see
HTMLTestingMailSystem). That's what I'll be doing now, I'll post the patch tomorrow which extends the test.Comment #18
LKS90 commentedAlright, here is a patch that extends test coverage with HTML formatting for contact messages. It uses the testing mail sender from simplenews (removed references specific for simplenews and fixed the documentation [which is copy/paste in simplenews]). I changed the test old test slightly, adding the special characters to subject and body. This requires me to split the mail body on line breaks to extract the message.
I'll take a look at the old patch, but I think that one is useless by now.
Comment #19
andypostSuppose this should live in one of contact\tests\modules
Comment #20
berdirI would move this this next to TestMailCollector and also name it accordingly.
As said above, the body itself should not be formatted, only the other part of the mail. I'll have to test this myself.
Comment #21
LKS90 commentedMoved the test, still passing even though it shouldn't (according to your second point in #20).
Comment #23
naveenvalechaStraight reroll against 8.1.x, we already have tests so removed the "Need tests" tag and it's 8.1.x target so also removed "Needs beta evaluation" tag as well.
Comment #24
pminfApplying the 2223967-23.patch from #23 failed for Drupal 8.1.8:
Comment #25
andypostComment #26
andypostI think composer changes unrelated, and should be removed from patch
Comment #27
oleksiyReroll of the previous patch #23
Comment #30
andypostComment #31
naveenvalechaRemoe this docblock . this is not needed
use PHP_EOL
Comment #32
claudiu.cristeaI see here only the test but nothing seems fixed.
s/HTMLTestMailCollector/HtmlTestMailCollector. Better TestHtmlMailCollector?
Should be removed.
This seem identical to
\Drupal\Core\Mail\Plugin\Mail\TestMailCollector::mail(). Why not extending TestMailCollector to reuse the code?Probably this deserves a comment, explaining what are we testing here.
Comment #33
manuel garcia commentedComment #34
manuel garcia commentedAttempting to address #31.1, #31.2, #32.1, #32.2, #32.3
Please have a look
Still to do #32.4
Comment #37
replicaobscuraThis applied cleanly on 8.2.x, but no longer applies on 8.3 for me. Not positive why yet, if I get it solved I'll post more information or an updated patch.
Comment #38
replicaobscuraI hate to remove work that was done in the previous patch, but it seems
core/modules/contact/src/Tests/ContactPersonalTest.phpor an equivalent test file no longer exists. Simply removing the portion of the previous patch that modified that test makes it apply cleanly again in 8.3.Here's a patch that handles that, it's identical otherwise. I didn't hide the old patch yet, because that test might still belong somewhere, but I just needed a patch that applies for now.
Comment #39
manuel garcia commentedComment #40
jibran#38 is not correct reroll please reroll the #34 agian.
Comment #41
jofitzRe-rolled #34 (and threw in an interdiff from #38 for good measure).
Comment #44
jofitzCorrect test failures.
Fixed coding standards errors.
Comment #46
jofitzCorrect test failure.
Comment #48
jhedstromAll of the patches after #7 seem to be test-only changes, and don't jive with the actual bug fix described in the IS.
Comment #50
pminfReroll of #46
Comment #52
gogowitsch commentedI changed two things in the patch:
getUsername()withgetDisplayName(). I pickedgetDisplayName()as the human-centric replacement instead ofgetAccountName()as the database-centric function. In default installations, both return the same string.Comment #53
pminfI've addressed #48 by adding missing changes from #7 to patch from #52.
@Gogowitsch I've reverted your issue description changes because I don't think we should separate the original proposed resolution to another issue.
Comment #57
mxr576So I originally reported this problem as a security issue and we could not agree if it is a security issue or not if Drupal allows sending emails with unescapsed XSS (that email clients _should_ block). I used Mailhog when I discovered this which is a stupid email client/collector for sure. https://security.stackexchange.com/questions/12568/is-e-mail-a-direct-ve...
@berdir realised that this issue is in the public issue queue for 6 years so we decided to close the security issue and go public with my patch. This was my original report, just to give you more context what is happening:
My patch takes a different direction that the last one, because I think the
MailFormatHelper::htmlToText()should still be called if the input is a raw string and not an implementation of the MarkupInterface.Comment #59
gregglesOne clarification on this point:
The Security Team is as agreed as we get that it is not a security issue and can be fixed in public.
Comment #60
mxr576Yes, that is right. I have got a full explanation.
Comment #61
berdirI don't see why you'd want to make it dynamic and change the direction of this issue at this point. I think it is a safe assumption at that point that non-Markup html strings are going to be converted later on. Swiftmailer would either do that or apply a basic format, although there's some ongoing discussions about that in the swiftmailer issue queue.
People have been using the existing fix for years and it has explicit test coverage.
Comment #62
mxr576So if it is stable then why it has not been merged yet?
I can also live with that, because the mailer service should decide how it handles the outgoing message. Including the transformation/sanitization of the content. But I think there should be a better/explicit guidance about how mailer implementation should handle this situation and that would solve this problem. When I bumpped into this I remember I was not sure which companent should I blame or patch and based on some digging it seemed reasonable to keep the sanitization in Contact module, just make it work better or similar how others work.
Comment #63
adamps commentedI have just found this public issue after I raised another security issue - @mxr576 we think alike! OK fine, it is been agreed to solve this in public but I fully agree with the idea:
I strongly support to commit this ASAP and backport to D8.
Sounds like we have a plan we can all agree on, so I have hidden patch #57 and scheduled patch #53 for retest on D9.1. PhpMail calls
MailFormatHelper::htmlToText()without checkinginstanceof MarkupInterface. Personally I find that a bit strange, but we can't really change it now. So to be consistent the contact module must never convert.Yes I agree with you @mxr576. However this would be a separate issue I think. We all want this patch to be committed ASAP so let's not try and add any changes to it:-)
Comment #64
mxr576We all want
this patcha real fix to be committed ASAP =]Since #53 there is a
TestHtmlMailCollectorin Drupal core provided bymail_html_testmodule for example and I would also reconsider the need for those changes that has happened intestSendPersonalContactMessage().It was not a coincidence that the test in #57 was much lightweight than in #53.
Comment #65
adamps commentedSure it can make sense to improve the patch if there are good reasons. I would say that the key points are
In any case #53 fails on D9 so it needs work
Comment #66
jofitzRe-rolled patch from #53 for 9.1.x.
Comment #68
sja112 commentedI have updated the patch to fix the failed test case.
Comment #69
adamps commentedThanks @sja112
In #64 @mxr576 pointed out
So I propose that we need to fix this patch to use the existing one, rather than adding a duplicate one.
Comment #70
sja112 commented@adamps thanks for pointing out the issue. I missed this. I have updated the patch.
Comment #72
sja112 commentedUpdated patch to fix the failing tests.
Comment #73
adamps commentedGreat looks good to me.
Comment #74
alexpottI would be really great if the issue summary can be updated:
One thing that would be good to know is if another other mailer is relying on this code for security? Another thought is that if we're agreeing that it is the mailer's responsibility to ensure the message body is safe then we need to make this extremely explicit.
Let's use the PHPunit method here $this->assertEquals()
$this>assertStringContainsString() and there's no need for a message.
Comment #75
shaktikI am working on it.
Comment #76
Rkumar commentedEven href shouldn't be like this
<a href='http://d8.dev'>emails</a>and Please elaborate in comments about this
Comment #77
shaktikupdated as per comment #74
Comment #78
shaktikComment #79
alexpottThese changes are not part of the change. And are out-of-scope. Yes it's a bit odd because the same test has two styles but eventually assertEqual() will be removed so adding more makes more future work.
Also test assertions unrelated to this change are being removed here.
These should be assertStringContainsString()
Comment #80
adamps commentedThanks @alexpott.
I updated the issue summary and fixed the title: the problem is that we are encoding twice rather the decoding twice.
My feeling is that we are not changing an API; rather we are fixing code to match the existing API. (True the API is poorly defined, but we can deduce what it needs to be through logical analysis and examination of existing code.) So there isn't a direct BC problem. It's true that some sites might be relying on the current behaviour. It's likely not very common and I see no way to avoid it. See the IS for more discussion.
This change is strictly positive for security. It solves some security risks and doesn't introduce new ones. The previous issue title was misleading here.
I became maintainer for simplenews and then swiftmailer and for sure I can confirm that the mail API is lightly documented and hard to understand how to use accurately. It's a way bigger problem that just this one aspect though so I'm not sure if it's practical to consider it as part of this issue. Currently the entire API is specified in about 20 lines of code and only 2-3 lines per element, some elements not even mentioned. Could we raise a separate issue to cover this?
Comment #81
shaktikComment #82
alexpottWhat security risk does this solve? That's the bit I don't understand yet. As far as I can see this change results in html now being outputted by the 'mail' view mode of the contact message. Any code that was relying on this now has to expect potentially un-safe HTML there now. This looks like increasing risks, not lowering them.
Is it worth considering to add an html-mail view mode and use that instead. And deprecate the mail view mode mode in favour of this new one?
Comment #83
adamps commented@alexpott I'm happy to have a chat if that would be easier as I fear I am just repeating myself. I already tried to explain in the IS and I'm unsure how to explain much better, however I will try.
You do an amazing job to commit so many Core issues to help the community and I guess that means you likely spend little time in Contrib. Although Core doesn't support sending HTML mails they are working in Contrib. Swiftmailer module is installed on 37000 D8 sites. Modules such as Simplenews (18000 D8 sites) send messages that contain HTML. It all works with the current interfaces. I agree it's not very well documented currently how to do it. However in the case of unclear documentation then it makes sense to accept the widespread current usage. In any case, as far as I can see, the solution proposed in this patch is the only way that works as I tried to explain in the IS. The current code is clearly bugged as per point 2 of the IS problem/motivation.
I tried to explain that in the IS. The security risk is when using a Contrib mail plugin that generates HTML mails. An anonymous user can send contact form messages containing arbitrary unsafe HTML. The user types a message containing HTML; the contact module encodes it to SafeMarkup, then MessageViewBuilder decodes it again. However because the decode occurred in #post_render the output is still an implementation of SafeMarkup. This is a severe violation of the SafeMarkup interface as the markup is entirely unfiltered. Hence @mxr576 and I independently raised this as security issue but the security team cleared it for a public issue.
In comparison to the previous paragraph the HTML output is safe - it is a valid implementation of MarkupInterface.
I agree that a mail plug-in code could be missing the call to
MailFormatHelper::htmlToText(). However if so thenI don't really follow how that strategy would work - which view mode would actually be used by
contact_mail()? A new view mode sounds like a lot of work for every site that uses the contact module.Comment #84
Rkumar commented@shaktik Still, comments are pending to implement.
Comment #85
adamps commentedNew patch that address all the comments from #74, #76, #79.
I fixed the tests which wasn't testing anything in the original patch (it failed to send the message then made some asserts on the message from the first part of the test).
Good idea. I have improved the comment on
MailInterface::format().Good news for removing an worries about back-compatibility: the interface definition for $message refers to
hook_mail()which is very clear that the sender can pass plain text or markup. Therefore it seems there is no doubt that the plug-in must handle either. We are not changing the interface, we are simply improving the documentation.Comment #86
adamps commentedComment #88
adamps commentedComment #89
replicaobscuraHere's a version that applies on Drupal 8.9. I am not sure if it applies on 9.x at this point, or if #88 does for that matter, so I'll just leave this here for now without changing the status or hiding any other patches.
Comment #90
jonathanshawI think the patch is good, and I will RTBC if we can clarify a few things.
Basically I think the documentation introduced is great, but should be even more verbose.
Elsewhere in this issue this is called the "reference implementation". Maybe it's usfeul to keep referring to it.
The meaning of "converted, joined and wrapped" is not clear.
I'm not clear what "converted" and "desired output" mean here.
Presumably whether you can use these depends on what format your original text is?
I'd like to see this issue created before RTBC, and a note on how it relates to this issue. Does anything about the changes here make that issue a worse problem?
Comment #91
adamps commentedThanks @jonathanshaw. Here is another patch with an improved comment.
Regarding 5. I will raise it. No the problem has not become worse. There is only a small relationship between the two issues: this issue improves the interface comment and so makes it more obvious that PhpMail does not obey the interface.
Comment #92
jonathanshawComment #93
adamps commentedGreat thanks. I raised #3174760: Mails resembling HTML are corrupted
Comment #96
catchCommitted/pushed to 9.1.x and backported to 9.0.x. Needs a re-roll for 8.9.x
Comment #97
adamps commentedGreat many thanks @catch
Comment #98
andypost8.9.x branch tests are broken, still waiting for test
Comment #99
andypostThe only change from commited patch is 8.9.x still using public visibility for $modules
Here's interdiff and RTBC
Comment #100
mxr576RTBC++
Comment #102
catchCommitted 05083ea and pushed to 8.9.x. Thanks!
Comment #103
adamps commentedGreat many thanks @catch
Comment #105
chi commentedThe formating of contact messages is kind of broken now. Note multiple new lines and weird indentation.