Skip to content

Commit

Permalink
Add escapeParameterHtml parameter. kazupon#1002
Browse files Browse the repository at this point in the history
  • Loading branch information
Gardar Hauksson committed Oct 6, 2020
1 parent 5ba214d commit 614fc8e
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 1 deletion.
1 change: 1 addition & 0 deletions decls/i18n.js
Expand Up @@ -92,6 +92,7 @@ declare type I18nOptions = {
sharedMessages?: LocaleMessage,
postTranslation?: PostTranslationHandler,
componentInstanceCreatedListener?: ComponentInstanceCreatedListener,
escapeParameterHtml?: boolean,
};

declare type IntlAvailability = {
Expand Down
34 changes: 34 additions & 0 deletions examples/formatting/escape-parameter-html/index.html
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Escape parameter HTML example</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-i18n@8.22.0/dist/vue-i18n.esm.browser.js"></script>
</head>
<body>
<div id="app">
<p v-html="displayMsg"></p>
</div>
<script>
const i18n = new VueI18n({
locale: 'en',
escapeParameterHtml: true,
messages: {
en: {
message: 'User input: <b>{value}</b>',
}
}
})
new Vue({
el: '#app',
i18n: i18n,
computed: {
displayMsg() {
return this.$t('message', {value: '<img src="" onError="alert(42)">'})
}
}
})
</script>
</body>
</html>
17 changes: 16 additions & 1 deletion src/index.js
Expand Up @@ -16,7 +16,8 @@ import {
remove,
includes,
merge,
numberFormatKeys
numberFormatKeys,
escapeParams
} from './util'
import BaseFormatter from './format'
import I18nPath from './path'
Expand Down Expand Up @@ -59,6 +60,7 @@ export default class VueI18n {
_componentInstanceCreatedListener: ?ComponentInstanceCreatedListener
_preserveDirectiveContent: boolean
_warnHtmlInMessage: WarnHtmlInMessageLevel
_escapeParameterHtml: boolean
_postTranslation: ?PostTranslationHandler
pluralizationRules: {
[lang: string]: (choice: number, choicesLength: number) => number
Expand Down Expand Up @@ -111,6 +113,7 @@ export default class VueI18n {
this.pluralizationRules = options.pluralizationRules || {}
this._warnHtmlInMessage = options.warnHtmlInMessage || 'off'
this._postTranslation = options.postTranslation || null
this._escapeParameterHtml = options.escapeParameterHtml || false

/**
* @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
Expand Down Expand Up @@ -345,6 +348,10 @@ export default class VueI18n {

if (this._formatFallbackMessages) {
const parsedArgs = parseArgs(...values)
if(this._escapeParameterHtml) {
parsedArgs.params = escapeParams(parsedArgs.params)
}

return this._render(key, interpolateMode, parsedArgs.params, key)
} else {
return key
Expand Down Expand Up @@ -650,6 +657,10 @@ export default class VueI18n {
if (!key) { return '' }
const parsedArgs = parseArgs(...values)
if(this._escapeParameterHtml) {
parsedArgs.params = escapeParams(parsedArgs.params)
}
const locale: Locale = parsedArgs.locale || _locale
let ret: any = this._translate(
Expand Down Expand Up @@ -716,6 +727,10 @@ export default class VueI18n {
const predefined = { 'count': choice, 'n': choice }
const parsedArgs = parseArgs(...values)
if(this._escapeParameterHtml) {
parsedArgs.params = escapeParams(parsedArgs.params)
}
parsedArgs.params = Object.assign(predefined, parsedArgs.params)
values = parsedArgs.locale === null ? [parsedArgs.params] : [parsedArgs.locale, parsedArgs.params]
return this.fetchChoice(this._t(key, _locale, messages, host, ...values), choice)
Expand Down
31 changes: 31 additions & 0 deletions src/util.js
Expand Up @@ -167,3 +167,34 @@ export function looseEqual (a: any, b: any): boolean {
return false
}
}

/**
* Sanitizes html special characters from input strings. For mitigating risk of XSS attacks.
* @param rawText The raw input from the user that should be escaped.
*/
function escapeHtml(rawText: string): string {
return rawText
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
/**
* Escapes html tags and special symbols from all provided params which were returned from parseArgs().params.
* This method performs an in-place replacement of the params object and returns the passed object.
*
* @param {any} params Parameters as provided from `parseArgs().params`.
* May be either an array of strings or a string->value map.
*/
export function escapeParams(params: any): any {
if(params != null) {
Object.keys(params).forEach(key => {
if(typeof(params[key]) == 'string') {
params[key] = escapeHtml(params[key])
}
})
}
return params
}
27 changes: 27 additions & 0 deletions test/unit/escape_parameter_html.test.js
@@ -0,0 +1,27 @@
const messages = {
en: {
listformat: '{0}',
nameformat: '{key}',
}
}

describe('escapeParameterHtml', () => {
it('Replacement parameters are escaped when escapeParameterHtml: true.', () => {
const i18n = new VueI18n({
locale: 'en',
messages,
escapeParameterHtml: true
})
assert(i18n.t('nameformat', { key: '<&"\'>' }) === '&lt;&amp;&quot;&apos;&gt;')
assert(i18n.t('listformat', ['<&"\'>']) === '&lt;&amp;&quot;&apos;&gt;')
})
it('Replacement parameters are not escaped when escapeParameterHtml: undefined.', () => {
const i18n = new VueI18n({
locale: 'en',
messages,
})
assert(i18n.t('nameformat', { key: '<&"\'>' }) === '<&"\'>')
assert(i18n.t('listformat', ['<&"\'>']) === '<&"\'>')

})
})
1 change: 1 addition & 0 deletions types/index.d.ts
Expand Up @@ -120,6 +120,7 @@ declare namespace VueI18n {
sharedMessages?: LocaleMessages;
postTranslation?: PostTranslationHandler;
componentInstanceCreatedListener?: ComponentInstanceCreatedListener;
escapeParameterHtml?: boolean;
}
}

Expand Down
17 changes: 17 additions & 0 deletions vuepress/api/README.md
Expand Up @@ -373,6 +373,23 @@ A handler for getting notified when component-local instance was created. The ha
This handler is useful when extending the root VueI18n instance and wanting to also apply those extensions to component-local instance.
#### espaceParameterHtml
> 8.22+
* **Type:** `Boolean`
* **Default:** `false`
If `escapeParameterHtml` is configured as true then interpolation parameters are escaped before the message is translated.
This is useful when translation output is used in `v-html` and the translation resource contains html markup (e.g. `<b>`
around a user provided value). This usage pattern mostly occurs when passing precomputed text strings into UI compontents.
The escape process involves replacing the following symbols with their respective HTML character entities: `<`, `>`, `"`, `'`, `&`.
Setting `escapeParameterHtml` as true should not break existing functionality but provides a safeguard against a subtle
type of XSS attack vectors.
### Properties
#### locale
Expand Down

0 comments on commit 614fc8e

Please sign in to comment.