diff --git a/decls/i18n.js b/decls/i18n.js
index 16fa8e4de..78b918e2e 100644
--- a/decls/i18n.js
+++ b/decls/i18n.js
@@ -92,6 +92,7 @@ declare type I18nOptions = {
sharedMessages?: LocaleMessage,
postTranslation?: PostTranslationHandler,
componentInstanceCreatedListener?: ComponentInstanceCreatedListener,
+ escapeParameterHtml?: boolean,
};
declare type IntlAvailability = {
diff --git a/examples/formatting/escape-parameter-html/index.html b/examples/formatting/escape-parameter-html/index.html
new file mode 100644
index 000000000..a3642646c
--- /dev/null
+++ b/examples/formatting/escape-parameter-html/index.html
@@ -0,0 +1,34 @@
+
+
+
+
+ Escape parameter HTML example
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index c8c878c54..8a3b1dd98 100644
--- a/src/index.js
+++ b/src/index.js
@@ -16,7 +16,8 @@ import {
remove,
includes,
merge,
- numberFormatKeys
+ numberFormatKeys,
+ escapeParams
} from './util'
import BaseFormatter from './format'
import I18nPath from './path'
@@ -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
@@ -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)`
@@ -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(
diff --git a/src/util.js b/src/util.js
index e5106d406..dbcdecd26 100644
--- a/src/util.js
+++ b/src/util.js
@@ -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, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+}
+
+/**
+ * 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
+}
\ No newline at end of file
diff --git a/test/unit/escape_parameter_html.test.js b/test/unit/escape_parameter_html.test.js
new file mode 100644
index 000000000..8a07b5571
--- /dev/null
+++ b/test/unit/escape_parameter_html.test.js
@@ -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: '<&"\'>' }) === '<&"'>')
+ assert(i18n.t('listformat', ['<&"\'>']) === '<&"'>')
+ assert(i18n.tc('nameformat', 1, { key: '<&"\'>' }).toString() === '<&"'>')
+ assert(i18n.tc('listformat', 1, ['<&"\'>']).toString() === '<&"'>')
+ })
+ 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', ['<&"\'>']) === '<&"\'>')
+
+ })
+})
diff --git a/types/index.d.ts b/types/index.d.ts
index 7f7327ec6..43619c5f2 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -120,6 +120,7 @@ declare namespace VueI18n {
sharedMessages?: LocaleMessages;
postTranslation?: PostTranslationHandler;
componentInstanceCreatedListener?: ComponentInstanceCreatedListener;
+ escapeParameterHtml?: boolean;
}
}
diff --git a/vuepress/api/README.md b/vuepress/api/README.md
index 36c61bf03..805662dd6 100644
--- a/vuepress/api/README.md
+++ b/vuepress/api/README.md
@@ -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. ``
+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