Ingested t() strings are normally validated on input for XSS (as opposed to user input that's filtered on output)
For example in D7: https://api.drupal.org/api/drupal/modules%21locale%21locale.admin.inc/fu... calls to https://api.drupal.org/api/drupal/includes%21locale.inc/function/locale_...
This validation needs to be applied to all strings submitted for config translation.
Example STR:
1. add a second language
2. give user A permission 'translate configuration'
3. as user A go to /admin/structure/comment/manage/comment/fields/comment.comment.comment_body/translate
4. add translation and fill label textfield with payload
5. script will trigger if admin visits /admin/config/regional/config-translation/comment_fields in the target language
Analysis thanks to Gabor Hojtsy:
the call stack of ConfigTranslationListController::listing() goes to EntityType::getLabel() which is a (string) $this->label and $this->label is…. a TranslationWrapper
it is a TranslationWrapper but is not with an already translated string… basically it translates the entity type “Comment”, and because we got in XSS via the config translation UI, we got a resulting string that is not safe, even though t() adds results to safe markup
Issues related to this were reported multiple times in the Drupal 8 security bug bounty program, including:
https://tracker.bugcrowd.com/submissions/5784d8a9dc93ce674776af30bf97f49...
https://tracker.bugcrowd.com/submissions/672a7ac983d1d6e554114e2f287824a...
credit to users:
JvE
grisendo
Comment | File | Size | Author |
---|---|---|---|
#86 | interdiff-2512466-83-85.txt | 191 bytes | Bhanu951 |
#83 | interdiff_81-83.txt | 1.12 KB | sahil.goyal |
#83 | 2512466-83.patch | 14.4 KB | sahil.goyal |
#80 | interdiff_77-80.txt | 787 bytes | ranjith_kumar_k_u |
#80 | 2512466-80.patch | 13.05 KB | ranjith_kumar_k_u |
Issue fork drupal-2512466
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #1
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedComment #2
mikemiles86Comment #3
mikemiles86I may be misunderstanding what the issue is based on how easy it seemed to be to solve.
Here is a patch that adds a validateForm method to core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
The validation checks to see if the config values submitted are safe or not using locale_string_is_safe(), and if they are not then it displays a form error.
One thing, I was not sure of the best way to display the correct form field title (such as 'label'), since the labels from the $form object contain HTML markup. (ex: "Label <span class="hidden">(English)</span>", which is how the form error message would display it)
Comment #5
mikemiles86Comment #6
mikemiles86Comment #7
pbuyle CreditAttribution: pbuyle at Floe design + technologies commentedTests are failing, and produce some warning. One of the cause is likely that
locale_string_is_safe($value)
expect a string, while$value
may be an array. Something using<a href="http://php.net/manual/en/function.array-walk-recursive.php">array_walk_recursive()</a>
should be able to validate strings nested in any level of nested arrays.@mikemiles86, I'm at the #DrupalNorth sprint if you wish to do pair programming.
Comment #8
Anonymous (not verified) CreditAttribution: Anonymous as a volunteer commentedUpdating the issue tag to include a hashless DrupalNorth2015 on behalf of the Drupal North sprinting group.
Comment #9
mikemiles86I've updated my patch, since the previous one was failing tests.
It will now only validate against submitted string values. @pbuyle in comment #7 mentioned that the submitted field value could be an array and we might want to iterate through it. I agree, but I was not sure of the best approach to do so. Potentially how deep could a form field values array be?
I also updated to grab the field label and #name from the $form array to pass to the form error message.
Comment #10
dawehnerIn general we should add a test which checks that you can actually not enter invalid data as part of the translation.
Comment #11
JvE CreditAttribution: JvE at One Shoe commentedBugcrowd asked me to comment on this issue.
Also:
This gives me
The submitted Label <span class="visually-hidden">(Dutch)</span> value contains disallowed HTML
. Seems the #title is already rendered and can contain html.And:
/admin/structure/views/view/frontpage/translate goes about 8 levels deep and the patch only protects the top level Label and Description. Setting to needs work.
Comment #12
mikemiles86You are correct. It seems that when the config form gets built, the field label contains that language span tag. Previously I was just using the $key value from the form_state values array, however that value does not always match the field label that is displayed.
I have yet to be able to find a reliable way to either get the field title without the language specific HTML or strip out the HTML. Any ideas are welcomed.
Thanks for this info. Could write a function that recursively checks the $value array to make sure it contains no unsafe strings. I'm not sure the best place for that function, a private function within the ConfigTranslationFormBase.php class?
Something such as:
Comment #13
mparker17Removing old hashed tag.
Comment #14
pbuyle CreditAttribution: pbuyle at Floe design + technologies commentedYou don't really need a recursive function,
array_walk_recursive()
will do recursive part for you.Comment #15
Gábor HojtsyBring on D8MI sprint.
Comment #16
Gábor HojtsyI don't think that following #2512460: "Translate user edited configuration" permission needs to be marked as restricted that landed earlier last week this one would be still critical given that XSS attacks from trusted permissions are not considered XSS attacks. I still believe that this filtering would ideally be added, but don't think this is a critical still.
Comment #17
mikemiles86Ok I've updated my patch to use array_walk_recursive() to check the submitted value (is it is an array).
I have also reworded the error message to be a bit more generic, so that do not have to worry about the display of the field label in the message.
Still having a hard time with Drupal highlighting the field with the error on it. I'm following the conventions for targeting the field by name, but it does not seem to work. Anyone able to help with this? find the right pattern?
Also as @dawehner mentioned and tagged, we should create a test for this use case. I'm not the best at writing tests so I'd welcome others on that front.
Comment #18
Gábor HojtsyAs per the docs:
So you should replace the [ after translation with ][. Basicaly ][ is the separator between the parent keys here.
Comment #19
mikemiles86D'oh. Wow, I cannot believe I missed that. Thanks for pointing it out Gabor.
New patch with interdiff attacched. All we need now is a test!
Comment #20
Gábor HojtsyLet's run testbot on it!
Comment #21
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedI think it should be critical since we in the past issued a SA for essentially the same vulnerability in core SA-CORE-2010-001
Comment #22
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedShould we have this validation at the API level instead of the form level?
I guess .po file imports are already covered by \Drupal\locale\PoDatabaseWriter ?
Comment #23
Gábor Hojtsy@pwolanin: do we release SAs for attacks performed with trusted/restricted permissions? I don't think so based on https://www.drupal.org/security-advisory-policy
Comment #24
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedHmm, actually that wasn't the SA I was thinking of - didn't we have one for the filtering of .po files?
Since we didn't add config translation to that list of permissions (yet) I guess this is a bit of a gray area.
Comment #25
Gábor HojtsyHere is an updated figure based on the one I posted at #2512460: "Translate user edited configuration" permission needs to be marked as restricted.
Comment #26
pwolanin CreditAttribution: pwolanin as a volunteer and at Acquia commentedSo I think we ned to solve this before release - so that's why I'd call it critical, but I don't feel that strongly
Comment #27
Gábor HojtsyI think I made my points. Marking for maintainer triage.
Comment #28
Gábor HojtsyBeen told that the tag is superfluous and the issue will be triaged anyway.
Comment #29
Gábor Hojtsy@pwolanin: "resolving" this issue with the XSS filtering used in locale will only make it as safe as locale, which still allows translators to own a site which is why we say it as such on https://www.drupal.org/security-advisory-policy, so I am not sure if before this issue XSS was possible and after this XSS is possible would qualify this as a critical issue.
Comment #30
JvE CreditAttribution: JvE at iO commentedHow bad of a performance hit would it be to xss filter all translations in TranslationManager?
That way it doesn't matter what translation permissions people have or where the po files came from.
Could this be made to be an option? (slow safe translations vs fast unsafe translations)
Comment #31
Gábor Hojtsy@JvE: that would not solve the problem either AFAIS. The underlying issue is that we cannot assume HTML output and that a translated string will be used at a HTML tag level. The output may be XML, JSON, a HTTP header, whatever and even in HTML it may be in an attribute. Do you see a way to do API level filtering for ensuring values that do not allow you to break out of HTML attributes or XML or JSON and inject malicious content or inject HTTP headers or unwanted HTML tags and whatever other output? The validation used in locale only covers the HTML tag case and so would this issue.
The XSS filtering in locale on form input and .po import is a best effort safety measure for the 80% case of attacks but is far from being a complete solution, therefore the explanation in https://www.drupal.org/security-advisory-policy that interface translation allows you to own the site. As explained in #25, the overlap with config translation makes config translation equally dangerous. I did make a mistake in #2512460: "Translate user edited configuration" permission needs to be marked as restricted assuming that marking it as a restricted permission will signal this but the letter of the policy does not indicate that, it would need to be listed on its own in https://www.drupal.org/security-advisory-policy.
I don't believe that the proposed ways so far to resolve this issue would resolve the full spectrum of attacks possible (which we did not see good ways with locale either, therefore the listing as a dangerous permission), so that is why I don't see how it is a critical issue to port the best effort validation for the common cases from locale, because it is not complete there either. It does not matter if the inadequate filtering is done on the API level or the UI level, its inadequate.
Comment #32
Gábor HojtsyBTW the HTML attribute question is mitigated mostly by AttributeString::__toString() in Drupal 8 escaping quotes. So that works for rendered markup. At least the possible attack vectors I tried did not work... There are still plenty of places where HTML is assembled manually, eg. looking through hook_help() may uncover opportunities for going into HTML attributes, and then of course the wast array of contribs. (And then this is just the HTML stuff).
Comment #33
catchAgreed with Gabor with one possible exception documented below. We should do this, but it's 'just' hardening with the permissions change.
The only thing that makes me slightly hesitant is that existing Drupal 8 sites might have assigned this permission, without it having been restricted - then later someone untrustworthy gets granted the role and bam!.
Without the validation, that's a potential lurking vulnerability in sites installed prior to #2512460: "Translate user edited configuration" permission needs to be marked as restricted. It's also something that could survive a complete re-install since you could have that role/permission in exported config.
With the validation, then only a site that was actually compromised prior to #2512460: "Translate user edited configuration" permission needs to be marked as restricted would be affected, and tough luck for them 'cos beta.
I don't mind either way, but wanted to document that nagging feeling on the issue. This isn't really about the hardening itself being critical or not, but whether we provide an upgrade path/backwards compatibility (via the hardening) for existing installs/default config.
Comment #34
xjmAlright, downgrading based on discussion with @catch, @Gábor Hojtsy, @effulgentsia, and @alexpott as per the above.
I do agree that we should create that change record. I reopened #2512460: "Translate user edited configuration" permission needs to be marked as restricted for that.
Comment #35
mikemiles86Comment #36
mikemiles86Based on the comment thread, this issue seems to be tied to a bigger discussion. With the scope of this issue around validating when config translations are input, is the patch/approach I have provided still viable? Or do we need a patch that goes into a different direction?
Comment #37
Gábor Hojtsy@mikemiles86: I think the patch makes sense as a hardening for config translation. Locale does not provide a more complete solution either. The only caveat I see with the validation is there are the text_format typed elements, which result in config filtered on output with an input format, so will filter/escape on output for sure (and may need to allow for inline JS, etc). So those should be skipped in validation.
Comment #38
tstoecklerMinor, but is the call out to
getConfigNames()
really necessary? I.e. are we expecting something in$form_values
that is not ingetConfigNames()
?Haven't tried this out but it seems the error message is confusing in case the value was in fact an array?
Comment #39
tstoecklerYeah, the error setting is not going to fly in that way. See the attached screenshot. If malicious input is found inside of a nested element all elements within the top-level one are being marked as having an error.
We should add the validation to the form elements themselves, i.e. we need a corresponding method on config translation's ElementInterface.
Working on that now.
Comment #40
tstoecklerHere we go. I think something along these lines is a little bit cleaner, and as proven by the screenshot solves the problem noted above.
Still needs tests.
Comment #42
Gábor HojtsyLooks better! Also looks like the error message text needs HTML removed still :)
Comment #43
tstoecklerHere we go.
Re, the error message: Yes, but that's a separate issue, no?
Comment #44
tstoeckler#fail
Comment #45
Gábor Hojtsy"Re, the error message: Yes, but that's a separate issue, no?", this issue introduces the error message, so it should not introduce one with escaped HTML in it AFAIS.
Comment #46
tstoecklerHere we go. Fixed the escaping issue.
Comment #48
tstoecklerMeh,
FormErrorHandler
really is a weird beast. But it really makes no sense to escape form element titles as is proven by this example.I think I've found an acceptable - albeit not exactly super nice - solution.
Comment #50
dawehnerSo this allows to also use HTML, but still have some sort of safe HTML.
Nitpick: out of scope ...
It would be great to document that/why we went with the approach to merge the two translation pagers to one.
Do we know whether this is an array?
What about using htmlspecialchars so we don't need another Safemarkup call
Comment #51
tstoecklerComment #52
tstoecklerThanks for the review. This should fix the mentioned items and hopefully also the tests. I had included the patch from #2409701: Field storage configuration is not exposed to config translation UI in the last one, that's why the removal of that is now in the interdiff. This one should be cleaner.
Comment #54
tstoecklerLet's see if this fixes the test.
Comment #55
Jose Reyero CreditAttribution: Jose Reyero as a volunteer commentedHi, maybe late, but just some thoughts about this one:
- Whatever validation we do, it should be applied to the original config strings too (the one editable by the admin). Otherwise we end up with config strings that just cannot be translated as they deserve. I.e. source string containing HTML that cannot be translated. Wouldn't it make sense to just prevent this HTML in the original string in the first place?
- An alternate, more flexible solution would be to apply validation or not depending on user role/permissions. This way, if the admin wants to add the same -dirty- html to the translation that is in the original string, he could. So there are strings that cannot be translated by regular translators but the admin should be able to translate them. Same for locale.
- Provided "Translate configuration" is a restricted permission... I don't think this makes too much sense anyway. (Gábor #16). Let's say regular translators go through some validation with the locale UI, but "configuration translators" can skip that validation -and yes, add their js or their XSS-
Whatever, my thought is: if we want to say Drupal 8 is fully translatable, we don't allow strings anywhere that are not translatable. Or if we allow a string in the configuration it *must* be translatable.
Can we just make an effort, relax, and assume "restricted permissions" allow users to do their own XSS?
Comment #56
Gábor Hojtsy@Jose: I don't think we can (or should) filter all config for XSS generally, that would be possibly problematic for things with an input format for example (eg. views header text), so at least it would need to be element specific. Also elements may be entirely escaped when used and may want to allow "HTML tags", eg. if your label for a views table column includes "Uses of
tags in posts", etc, which is displayed escaped. The sole reason this is open IMHO is to provide parity with locale, since some strings would not be possible to enter via the locale UI for the same setting translations which are possible to enter on the config translation UI, so the behavior is inconsistent.Comment #57
Jose Reyero CreditAttribution: Jose Reyero as a volunteer commented@Gábor,
Ok, I get it.
The main issue I'm pointing out is that if we don't allow the same markup for the configuration translation than for the original configuration the functionality is inconsistent too. And worse, it is a more limiting issue.
On top of that... what is a configuration translation / what is the original configuration? Well, that depends on the default language of the site actually, this adds up to the inconsistencies.
About locale translation / UI consistency maybe we better just keep strings out of it when the source translation doesn't pass 'locale_string_is_safe' validation. That would be better consistency if that is our main concern. And better usability, because it is not easily understandable when there's a source string you cannot translate using the same format.
Comment #58
tstoecklerI agree with @Jose Reyero: If we don't check whether the source config passes the
locale_string_is_safe()
test, we are presenting the user with an impossible to complete UI if make the translation pass that test.Here's an attempt at solving that: If the source config is not safe, simply display an error message. The rest of the config can still be translated normally. If people agree here on the basic approach, we should probably get a usability review on this.
I also updated the
PluralVariant
form element for the validation added here now that #2454829: Configuration translation UI does not support plural sources/targets is in.Still needs tests btw.
Comment #59
Jose Reyero CreditAttribution: Jose Reyero as a volunteer commented@tstoeckler,
That's certainly some usability improvement like "hey guy, do not try to translate this, you'll get stuck". We shouldn't be able to reach this point though. The same validation should be used for config editor and config translator (either for both of them or for none of them).
IMO we better forget about configuration translation and just fix it in the locale translation UI. Since both of them need different permissions we should be fine.
Then, what about locale_string_is_safe() being used for validating *source* strings:
a) in po extractor
b) before adding any source string to locale tables
and throwing some big "You cannot use t() for that ugly string" error (Feel free to use the string in your module without 't' but don't mess with translators' life please).
So, short points:
1. Untranslatable strings should never be fed into the locale system. This is a BUG.
2. If they can be added to configuration, they must be translatable somehow. (They are now so we are fine as it is)
There's still the issue of (1) untranslatable strings being added to configuration, (2) configuration exported to module/profile, and (3) module/profile installed somewhere else. At this point the locale system will try to add that source string and it should throw at least a WARNING: You are intalling some configuration that is not translatable.
I still think this is no security issue, just a usability one.
(There's also the BIG usability issue of putting HTML In front of translators who may know a few languages but none of them is HTML. This one is for Drupal 9 I guess...)
Comment #60
tstoecklerI fail to see how that would be possible. In Config Translation it's both possible and needed because the forms are entirely auto-generated, so we cannot employ custom validation for those strings that we know will end up on the user interface. I don't think it's possible to perform this validation generically for all configuration.
Regarding your suggestion: Are you basically proposing to move the
locale_string_is_safe()
check intoLocaleConfigSubscriber::saveCustomizedTranslation()
? The problem with that is that you would enter things in Config Translation and they would either end up in Locale or not depending on what you put in. I think that could confuse users?Comment #61
Jose Reyero CreditAttribution: Jose Reyero as a volunteer commentedWell, the 'none of them' option is an empty patch :-)
For the other option, it would mean generalizing the 'locale_string_is_safe' to something like 'config_string_is_safe' to be used for config/string validation that would need to be used also when Locale is not enabled, certainly way too complex... And anyway as an occasional site admin the last thing I'd like to see is my configuration options being limited . But as a site translator I would see not being able to translate that configuration string as a bug.
The point I'm trying to make here is why this cannot be fixed by only limiting "configuration translation" because that would introduce worse inconsistencies. Still, we can fix inconsistencies in the locale system not allowing such strings into it.
Maybe the title of this issue is just misleading and instead of "Config translation needs to be validated..." it should be "Some configuration strings cannot be translated using the Interface translation page / with regular interface translation permission"
No, I don't think config translation needs to be validated at all -at least no more than configuration itself.
Comment #62
Gábor HojtsyRemoving from sprint. Last worked on 5 months ago.
Comment #73
larowlanTagging to yield some eyes
Comment #76
needs-review-queue-bot CreditAttribution: needs-review-queue-bot as a volunteer commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #77
karishmaamin CreditAttribution: karishmaamin at Specbee commentedRe-rolled patch against 10.1.x.Please review
Comment #78
karishmaamin CreditAttribution: karishmaamin at Specbee commentedComment #79
smustgrave CreditAttribution: smustgrave at Mobomo commentedCI failures.
Did not review yet.
Comment #80
ranjith_kumar_k_u CreditAttribution: ranjith_kumar_k_u as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedFixed custom command failure
Comment #81
Krishnapal singh21 CreditAttribution: Krishnapal singh21 at OpenSense Labs for DrupalFit commentedUploading patch after fixing custom command failure
Comment #82
needs-review-queue-bot CreditAttribution: needs-review-queue-bot as a volunteer commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #83
sahil.goyal CreditAttribution: sahil.goyal as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedI noticed that the error pattern #^Variable \$form_output in empty\(\) always exists and is not falsy\.$# is being ignored in the file /var/www/html/core/modules/system/tests/src/Functional/Form/FormTest.php. I understand that this is a false positive caused by the Drupal FormTestBase class. This class includes a method called assertEmpty(), which checks that a variable is both empty and not falsey.
Comment #84
Bhanu951 CreditAttribution: Bhanu951 as a volunteer commentedComment #86
Bhanu951 CreditAttribution: Bhanu951 as a volunteer commentedRerolled 2512466-83.patch against 10.1.x and updated dictionary.txt file.
Comment #87
Bhanu951 CreditAttribution: Bhanu951 as a volunteer commentedComment #88
smustgrave CreditAttribution: smustgrave at Mobomo commentedLeft a small comment and fails appear legit.
Comment #90
mohit_aghera CreditAttribution: mohit_aghera at PreviousNext commentedComment #91
smustgrave CreditAttribution: smustgrave at Mobomo commentedGlancing at the issue summary and seem it could use a little love.
Specifically what is the proposed solution.
Testing steps still the same?
Not 100% sure if the new function for the FormElement may need a change record.