fix(client): All template writes are by default HTML escaped. #4296
Conversation
| */ | ||
| unsafeTranslate (text, context = this.getContext()) { | ||
| if (Strings.hasUnsafeVariables(text)) { | ||
| throw new Error(`Unsafe string to translate: ${text}`); |
shane-tomlinson
Oct 18, 2016
Author
Member
Instead of a hard exception, maybe just log these for a train to see if we missed any?
Instead of a hard exception, maybe just log these for a train to see if we missed any?
be82ecd
to
9a6ad53
| * @returns {Boolean} true if string has HTML, false otw. | ||
| */ | ||
| function hasHTML(string) { | ||
| if (/&\D+;/i.test(string)) { |
shane-tomlinson
Oct 19, 2016
Author
Member
Extract these regexp's into constants
Extract these regexp's into constants
| */ | ||
| unsafeTranslate (text, context = this.getContext()) { | ||
| if (Strings.hasUnsafeVariables(text)) { | ||
| const err = AuthErrors.toError('UNSAFE_VARIABLE_NAME'); |
shane-tomlinson
Oct 19, 2016
Author
Member
Maybe rename the error to UNSAFE_INTERPOLATION_VARIABLE_NAME
Maybe rename the error to UNSAFE_INTERPOLATION_VARIABLE_NAME
| @@ -75,7 +75,7 @@ define(function (require, exports, module) { | |||
|
|
|||
| if (email && isOpenWebmailButtonVisible) { | |||
| _.extend(context, { | |||
| unsafeWebmailLink: this.getWebmailLink(email), | |||
| escapedWebmailLink: this.getWebmailLink(email), | |||
shane-tomlinson
Oct 19, 2016
Author
Member
I don't see where this is escaped!
I don't see where this is escaped!
| @@ -19,8 +19,7 @@ define(function (require, exports, module) { | |||
| const TOOLTIP_MESSAGES = { | |||
| FOCUS_PROMPT_MESSAGE: t('8 characters minimum, but longer if you plan to sync passwords.'), | |||
| INITIAL_PROMPT_MESSAGE: t('A strong, unique password will keep your Firefox data safe from intruders.'), | |||
| WARNING_PROMPT_MESSAGE: t('This is a common password; please consider another one.'), | |||
| WARNING_PROMPT_MESSAGE_WITH_LINK: t('This is a common password; please consider another one.') | |||
shane-tomlinson
Oct 19, 2016
Author
Member
the _WITH_LINK variant didn't have a link, so I consolidated the two messages.
the _WITH_LINK variant didn't have a link, so I consolidated the two messages.
| @@ -551,6 +597,14 @@ define(function (require, exports, module) { | |||
| }); | |||
| }); | |||
|
|
|||
| describe('unsafeDisplaySuccess', () => { | |||
| it('allows HTML in messages', () => { | |||
| const expected = 'a message<div>with html</div>'; | |||
shane-tomlinson
Oct 19, 2016
Author
Member
use HTML_MESSAGE instead.
use HTML_MESSAGE instead.
| view.unsafeTranslate(HTML_MESSAGE), UNESCAPED_HTML_TRANSLATION); | ||
| assert.isFalse(view.logError.called); | ||
|
|
||
| view.unsafeTranslate('%(unsafeVariableName)s'); |
shane-tomlinson
Oct 19, 2016
Author
Member
Extract this string to a constant.
Extract this string to a constant.
3a94829
to
3e085e5
|
@mozilla/fxa-devs - who wants it? |
|
I absolutely love the explicit naming: |
| if (Strings.hasHTML(text)) { | ||
| const err = AuthErrors.toError('HTML_WILL_BE_ESCAPED'); | ||
| err.string = text; | ||
| this.logError(err); |
seanmonstar
Oct 19, 2016
Member
In production does this error get sent to us, like sentry or something?
In production does this error get sent to us, like sentry or something?
shane-tomlinson
Oct 21, 2016
Author
Member
Yup, exactly, the string itself will be part of the message displayed in Sentry.
Yup, exactly, the string itself will be part of the message displayed in Sentry.
| var t = function (msg) { | ||
| return msg; | ||
| }; | ||
| const HTML_CHAR_CODE_REGEXP = /&\D+;/i; |
seanmonstar
Oct 19, 2016
Member
This won't catch when the escape code is numeric. > and ? are equivalent, this regex won't catch the latter.
This won't catch when the escape code is numeric. > and ? are equivalent, this regex won't catch the latter.
| * @returns {Boolean} true if has unsafe variables, false otw. | ||
| */ | ||
| function hasUnsafeVariables(string) { | ||
| if (UNNAMED_VARIABLE_REGEXP.test(string)) { |
seanmonstar
Oct 19, 2016
•
Member
This could use || instead:
return UNNAMED_VARIABLE_REGEXP.test(string) || UNSAFE_VARIABLE_REGEXP.test(string);
If you feel the ifs read more easily, ignore me.
This could use || instead:
return UNNAMED_VARIABLE_REGEXP.test(string) || UNSAFE_VARIABLE_REGEXP.test(string);If you feel the ifs read more easily, ignore me.
| const NAMED_VARIABLE_REGEXP = /\%\(([a-zA-Z]+)\)s/g; | ||
| const UNNAMED_VARIABLE_REGEXP = /%s/g; | ||
| // safe variables have an `escaped` prefix. | ||
| const UNSAFE_VARIABLE_REGEXP = /\%\(((?!escaped)[a-zA-Z]+)\)s/g; |
seanmonstar
Oct 19, 2016
Member
We've been moving away from a postfix-Hungarian notation for RegExps in the auth server. I don't know if that's desirable here as well.
We've been moving away from a postfix-Hungarian notation for RegExps in the auth server. I don't know if that's desirable here as well.
3e085e5
to
8aaa194
|
@seanmonstar - updated! |
8aaa194
to
47b1317
|
All tidy! Looks good from here, except that Travis went red. It might be because of the DNS outage, so I just gave it a kick. Merge when travis is green! |
47b1317
to
fd6a8d0
A new Mustache helper function `unsafeTranslate`, has been added for unsafe writes, to be used when inserting HTML. All interpolation variables passed to `unsafeTranslate` must be prefixed with `escaped` or else an exception is thrown. This is to remind the developer to escape the variable in the most appropriate way. The `t` helper function now HTML escapes by default. Rename some function/variable names for consistency: * `displaySuccessUnsafe` => `unsafeDisplaySuccess` * `displayErrorUnsafe` => `unsafeDisplayError` * `successUnsafe` => `unsafeSuccess`
fd6a8d0
to
06d994f
|
@shane-tomlinson does this fix the issue reported by cure53? |
|
@rfk - yes |

A new Mustache helper function
unsafeTranslate, has been addedfor unsafe writes, to be used when inserting HTML.
All interpolation variables passed to
unsafeTranslatemust beprefixed with
escapedor else an exception is thrown. This isto remind the developer to escape the variable in the most
appropriate way.
The
thelper function now HTML escapes by default.Rename some function/variable names for consistency:
displaySuccessUnsafe=>unsafeDisplaySuccessdisplayErrorUnsafe=>unsafeDisplayErrorsuccessUnsafe=>unsafeSuccess