Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add escapeParameterHtml parameter. #1002 #1009

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions decls/i18n.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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/[email protected]/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>
9 changes: 8 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
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 @@ -650,6 +653,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
33 changes: 33 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,36 @@ 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 operation on the params object.
*
* @param {any} params Parameters as provided from `parseArgs().params`.
* May be either an array of strings or a string->any map.
*
* @returns The manipulated `params` object.
*/
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
}
29 changes: 29 additions & 0 deletions test/unit/escape_parameter_html.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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;')
assert(i18n.tc('nameformat', 1, { key: '<&"\'>' }).toString() === '&lt;&amp;&quot;&apos;&gt;')
assert(i18n.tc('listformat', 1, ['<&"\'>']).toString() === '&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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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