CVE-2024-52809
Description
vue-i18n is an internationalization plugin for Vue.js. In affected versions vue-i18n can be passed locale messages to createI18n or useI18n. When locale message ASTs are generated in development mode there is a possibility of Cross-site Scripting attack. This issue has been addressed in versions 9.14.2, and 10.0.5. Users are advised to upgrade. There are no known workarounds for this vulnerability.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A stored XSS vulnerability in vue-i18n allows attackers to inject malicious code via prototype pollution when locale message ASTs are used in development mode.
Vulnerability
Overview
CVE-2024-52809 is a cross-site scripting (XSS) vulnerability in the vue-i18n internationalization plugin for Vue.js. The flaw arises when locale message ASTs (Abstract Syntax Trees) are generated during development mode. Specifically, the AST nodes contain special properties intended for performance optimization (e.g., the static property). If an attacker can achieve prototype pollution—modifying Object.prototype—they can inject properties that are unsafely processed by the message formatting code. The vulnerability affects versions before 9.14.2 and 10.0.5. [1][2][3]
Attack
Vector and Exploitation
Exploitation requires an attacker to first perform prototype pollution on the application’s JavaScript environment. This could occur through a malicious third-party script, a compromised dependency, or a separate vulnerability that enables prototype pollution. Once Object.prototype is polluted with a property like static containing an XSS payload, any call to the t or $t translation function in development mode can interpret that payload as part of the AST. The vulnerable code paths in format() and related functions do not properly guard against such pollutions, allowing the injected AST nodes to be rendered as HTML. The attack does not require authentication but depends on the user loading a page that uses vue-i18n with developer/optimized AST messages. [2][3]
Impact
If exploited, the XSS vulnerability allows an attacker to execute arbitrary JavaScript in the context of the victim’s browser session. This can lead to session hijacking, data theft, defacement, or other malicious actions. The CVSS score is Medium (6.1 per NVD), reflecting the prerequisite of prototype pollution which typically requires additional attack steps. [3][4]
Mitigation
The vue-i18n maintainers have addressed the issue in version 9.14.2 (for the 9.x branch) and version 10.0.5 (for the 10.x branch). The fix involves sanitizing AST node property access by using safer object creation (create()) instead of raw Object.create(null) patterns, preventing prototype-polluted properties from being automatically included. Users are strongly advised to upgrade to these patched versions. There are no known workarounds. Additionally, developers should avoid using untrusted or user-supplied locale messages in development mode and should review their application’s supply chain for potential prototype pollution vectors. [1][3][4]
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@intlify/core-basenpm | >= 9.3.0, < 9.14.2 | 9.14.2 |
vue-i18nnpm | >= 9.3.0, < 9.14.2 | 9.14.2 |
@intlify/corenpm | >= 9.3.0, < 9.14.2 | 9.14.2 |
@intlify/vue-i18n-corenpm | >= 9.3.0, < 9.14.2 | 9.14.2 |
petite-vue-i18nnpm | >= 10.0.0, < 10.0.5 | 10.0.5 |
@intlify/core-basenpm | >= 10.0.0, < 10.0.5 | 10.0.5 |
vue-i18nnpm | >= 10.0.0, < 10.0.5 | 10.0.5 |
@intlify/corenpm | >= 10.0.0, < 10.0.5 | 10.0.5 |
@intlify/vue-i18n-corenpm | >= 10.0.0, < 10.0.5 | 10.0.5 |
Affected products
7- ghsa-coords5 versionspkg:npm/%40intlify/corepkg:npm/%40intlify/core-basepkg:npm/%40intlify/vue-i18n-corepkg:npm/petite-vue-i18npkg:npm/vue-i18n
>= 9.3.0, < 9.14.2+ 4 more
- (no CPE)range: >= 9.3.0, < 9.14.2
- (no CPE)range: >= 9.3.0, < 9.14.2
- (no CPE)range: >= 9.3.0, < 9.14.2
- (no CPE)range: >= 10.0.0, < 10.0.5
- (no CPE)range: >= 9.3.0, < 9.14.2
Patches
4be77fccb0fe25448139375d19f20909ef8c9Merge commit from fork
20 files changed · +210 −159
packages/core-base/src/compilation.ts+3 −2 modified@@ -4,6 +4,7 @@ import { detectHtmlTag } from '@intlify/message-compiler' import { + create, format, hasOwn, isBoolean, @@ -32,10 +33,10 @@ function checkHtmlMessage(source: string, warnHtmlMessage?: boolean): void { } const defaultOnCacheKey = (message: string): string => message -let compileCache: unknown = Object.create(null) +let compileCache: unknown = create() export function clearCompileCache(): void { - compileCache = Object.create(null) + compileCache = create() } export function isMessageAST(val: unknown): val is ResourceNode {
packages/core-base/src/context.ts+11 −8 modified@@ -2,6 +2,7 @@ import { assign, + create, isArray, isBoolean, isFunction, @@ -507,23 +508,23 @@ export function createCoreContext<Message = string>(options: any = {}): any { : _locale const messages = isPlainObject(options.messages) ? options.messages - : { [_locale]: {} } + : createResources(_locale) const datetimeFormats = !__LITE__ ? isPlainObject(options.datetimeFormats) ? options.datetimeFormats - : { [_locale]: {} } - : { [_locale]: {} } + : createResources(_locale) + : createResources(_locale) const numberFormats = !__LITE__ ? isPlainObject(options.numberFormats) ? options.numberFormats - : { [_locale]: {} } - : { [_locale]: {} } + : createResources(_locale) + : createResources(_locale) const modifiers = assign( - {}, - options.modifiers || {}, + create(), + options.modifiers, getDefaultLinkedModifiers<Message>() ) - const pluralRules = options.pluralRules || {} + const pluralRules = options.pluralRules || create() const missing = isFunction(options.missing) ? options.missing : null const missingWarn = isBoolean(options.missingWarn) || isRegExp(options.missingWarn) @@ -628,6 +629,8 @@ export function createCoreContext<Message = string>(options: any = {}): any { return context } +const createResources = (locale: Locale) => ({ [locale]: create() }) + /** @internal */ export function isTranslateFallbackWarn( fallback: boolean | RegExp,
packages/core-base/src/datetime.ts+3 −2 modified@@ -1,5 +1,6 @@ import { assign, + create, isBoolean, isDate, isEmptyObject, @@ -322,8 +323,8 @@ export function parseDateTimeArgs( ...args: unknown[] ): [string, number | Date, DateTimeOptions, Intl.DateTimeFormatOptions] { const [arg1, arg2, arg3, arg4] = args - const options = {} as DateTimeOptions - let overrides = {} as Intl.DateTimeFormatOptions + const options = create() as DateTimeOptions + let overrides = create() as Intl.DateTimeFormatOptions let value: number | Date if (isString(arg1)) {
packages/core-base/src/number.ts+15 −14 modified@@ -1,31 +1,32 @@ import { - isString, + assign, + create, isBoolean, - isPlainObject, - isNumber, isEmptyObject, - assign + isNumber, + isPlainObject, + isString } from '@intlify/shared' import { handleMissing, isTranslateFallbackWarn, - NOT_REOSLVED, - MISSING_RESOLVE_VALUE + MISSING_RESOLVE_VALUE, + NOT_REOSLVED } from './context' -import { CoreWarnCodes, getWarnMessage } from './warnings' import { CoreErrorCodes, createCoreError } from './errors' -import { Availabilities } from './intl' import { getLocale } from './fallbacker' +import { Availabilities } from './intl' +import { CoreWarnCodes, getWarnMessage } from './warnings' -import type { Locale, FallbackLocale } from './runtime' +import type { CoreContext, CoreInternalContext } from './context' +import type { LocaleOptions } from './fallbacker' +import type { FallbackLocale, Locale } from './runtime' import type { NumberFormat, - NumberFormats as NumberFormatsType, NumberFormatOptions, + NumberFormats as NumberFormatsType, PickupFormatKeys } from './types' -import type { LocaleOptions } from './fallbacker' -import type { CoreContext, CoreInternalContext } from './context' /** * # number @@ -317,8 +318,8 @@ export function parseNumberArgs( ...args: unknown[] ): [string, number, NumberOptions, Intl.NumberFormatOptions] { const [arg1, arg2, arg3, arg4] = args - const options = {} as NumberOptions - let overrides = {} as Intl.NumberFormatOptions + const options = create() as NumberOptions + let overrides = create() as Intl.NumberFormatOptions if (!isNumber(arg1)) { throw createCoreError(CoreErrorCodes.INVALID_ARGUMENT)
packages/core-base/src/runtime.ts+3 −2 modified@@ -1,6 +1,7 @@ import { HelperNameMap } from '@intlify/message-compiler' import { assign, + create, isArray, isFunction, isNumber, @@ -360,7 +361,7 @@ export function createMessageContext<T = string, N = {}>( const list = (index: number): unknown => _list[index] // eslint-disable-next-line @typescript-eslint/no-explicit-any - const _named = options.named || ({} as any) + const _named = options.named || (create() as any) isNumber(options.pluralIndex) && normalizeNamed(pluralIndex, _named) const named = (key: string): unknown => _named[key] @@ -437,7 +438,7 @@ export function createMessageContext<T = string, N = {}>( [HelperNameMap.TYPE]: type, [HelperNameMap.INTERPOLATE]: interpolate, [HelperNameMap.NORMALIZE]: normalize, - [HelperNameMap.VALUES]: assign({}, _list, _named) + [HelperNameMap.VALUES]: assign(create(), _list, _named) } return ctx
packages/core-base/src/translate.ts+5 −4 modified@@ -1,5 +1,6 @@ import { assign, + create, escapeHtml, generateCodeFrame, generateFormatCacheKey, @@ -677,7 +678,7 @@ export function translate< : [ key, locale, - (messages as unknown as LocaleMessages<Message>)[locale] || {} + (messages as unknown as LocaleMessages<Message>)[locale] || create() ] // NOTE: // Fix to work around `ssrTransfrom` bug in Vite. @@ -830,7 +831,7 @@ function resolveMessageFormat<Messages, Message>( } = context const locales = localeFallbacker(context as any, fallbackLocale, locale) // eslint-disable-line @typescript-eslint/no-explicit-any - let message: LocaleMessageValue<Message> = {} + let message: LocaleMessageValue<Message> = create() let targetLocale: Locale | undefined let format: PathValue = null let from: Locale = locale @@ -869,7 +870,7 @@ function resolveMessageFormat<Messages, Message>( } message = - (messages as unknown as LocaleMessages<Message>)[targetLocale] || {} + (messages as unknown as LocaleMessages<Message>)[targetLocale] || create() // for vue-devtools timeline event let start: number | null = null @@ -1044,7 +1045,7 @@ export function parseTranslateArgs<Message = string>( ...args: unknown[] ): [Path | MessageFunction<Message> | ResourceNode, TranslateOptions] { const [arg1, arg2, arg3] = args - const options = {} as TranslateOptions + const options = create() as TranslateOptions if ( !isString(arg1) &&
packages/message-compiler/src/tokenizer.ts+5 −4 modified@@ -1,10 +1,10 @@ -import { createScanner, CHAR_SP as SPACE, CHAR_LF as NEW_LINE } from './scanner' +import { CompileErrorCodes, createCompileError } from './errors' import { createLocation, createPosition } from './location' -import { createCompileError, CompileErrorCodes } from './errors' +import { createScanner, CHAR_LF as NEW_LINE, CHAR_SP as SPACE } from './scanner' -import type { Scanner } from './scanner' -import type { SourceLocation, Position } from './location' +import type { Position, SourceLocation } from './location' import type { TokenizeOptions } from './options' +import type { Scanner } from './scanner' export const enum TokenTypes { Text, // 0 @@ -447,6 +447,7 @@ export function createTokenizer( function readText(scnr: Scanner): string { let buf = '' + while (true) { const ch = scnr.currentChar() if (
packages/shared/src/messages.ts+5 −2 modified@@ -1,4 +1,4 @@ -import { isArray, isObject } from './utils' +import { create, isArray, isObject } from './utils' const isNotObjectOrIsArray = (val: unknown) => !isObject(val) || isArray(val) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -14,10 +14,13 @@ export function deepCopy(src: any, des: any): void { // using `Object.keys` which skips prototype properties Object.keys(src).forEach(key => { + if (key === '__proto__') { + return + } // if src[key] is an object/array, set des[key] // to empty object/array to prevent setting by reference if (isObject(src[key]) && !isObject(des[key])) { - des[key] = Array.isArray(src[key]) ? [] : {} + des[key] = Array.isArray(src[key]) ? [] : create() } if (isNotObjectOrIsArray(des[key]) || isNotObjectOrIsArray(src[key])) {
packages/shared/src/utils.ts+4 −1 modified@@ -81,6 +81,9 @@ export const isEmptyObject = (val: unknown): val is boolean => export const assign = Object.assign +const _create = Object.create +export const create = (obj: object | null = null): object => _create(obj) + let _globalThis: any export const getGlobalThis = (): any => { // prettier-ignore @@ -95,7 +98,7 @@ export const getGlobalThis = (): any => { ? window : typeof global !== 'undefined' ? global - : {}) + : create()) ) }
packages/shared/test/messages.test.ts+31 −0 modified@@ -48,3 +48,34 @@ test('deepCopy merges without mutating src argument', () => { // should not mutate source object expect(msg1).toStrictEqual(copy1) }) + +describe('CVE-2024-52810', () => { + test('__proto__', () => { + const source = '{ "__proto__": { "pollutedKey": 123 } }' + const dest = {} + + deepCopy(JSON.parse(source), dest) + expect(dest).toEqual({}) + // @ts-ignore -- initialize polluted property + expect(JSON.parse(JSON.stringify({}.__proto__))).toEqual({}) + }) + + test('nest __proto__', () => { + const source = '{ "foo": { "__proto__": { "pollutedKey": 123 } } }' + const dest = {} + + deepCopy(JSON.parse(source), dest) + expect(dest).toEqual({ foo: {} }) + // @ts-ignore -- initialize polluted property + expect(JSON.parse(JSON.stringify({}.__proto__))).toEqual({}) + }) + + test('constructor prototype', () => { + const source = '{ "constructor": { "prototype": { "polluted": 1 } } }' + const dest = {} + + deepCopy(JSON.parse(source), dest) + // @ts-ignore -- initialize polluted property + expect({}.polluted).toBeUndefined() + }) +})
packages/vue-i18n-core/src/components/base.ts+1 −1 modified@@ -1,7 +1,7 @@ import { Composer } from '../composer' -import type { I18nScope } from '../i18n' import type { Locale } from '@intlify/core-base' +import type { I18nScope } from '../i18n' export type ComponentI18nScope = Exclude<I18nScope, 'local'>
packages/vue-i18n-core/src/components/DatetimeFormat.ts+5 −5 modified@@ -1,16 +1,16 @@ -import { defineComponent } from 'vue' -import { assign } from '@intlify/shared' import { DATETIME_FORMAT_OPTIONS_KEYS } from '@intlify/core-base' +import { assign } from '@intlify/shared' +import { defineComponent } from 'vue' import { useI18n } from '../i18n' import { DatetimePartsSymbol } from '../symbols' -import { renderFormatter } from './formatRenderer' import { baseFormatProps } from './base' +import { renderFormatter } from './formatRenderer' -import type { VNodeProps } from 'vue' import type { DateTimeOptions } from '@intlify/core-base' +import type { VNodeProps } from 'vue' import type { Composer, ComposerInternal } from '../composer' -import type { FormattableProps } from './formatRenderer' import type { BaseFormatProps } from './base' +import type { FormattableProps } from './formatRenderer' /** * DatetimeFormat Component Props
packages/vue-i18n-core/src/components/formatRenderer.ts+8 −8 modified@@ -1,15 +1,15 @@ +import { assign, create, isArray, isObject, isString } from '@intlify/shared' import { h } from 'vue' import { getFragmentableTag } from './utils' -import { isString, isObject, isArray, assign } from '@intlify/shared' +import type { DateTimeOptions, NumberOptions } from '@intlify/core-base' import type { RenderFunction, SetupContext, VNode, - VNodeChild, - VNodeArrayChildren + VNodeArrayChildren, + VNodeChild } from 'vue' -import type { NumberOptions, DateTimeOptions } from '@intlify/core-base' import type { BaseFormatProps } from './base' /** @@ -61,7 +61,7 @@ export function renderFormatter< return (): VNodeChild => { const options = { part: true } as Arg - let overrides = {} as FormatOverrideOptions + let overrides = create() as FormatOverrideOptions if (props.locale) { options.locale = props.locale @@ -78,9 +78,9 @@ export function renderFormatter< // Filter out number format options only overrides = Object.keys(props.format).reduce((options, prop) => { return slotKeys.includes(prop) - ? assign({}, options, { [prop]: (props.format as any)[prop] }) // eslint-disable-line @typescript-eslint/no-explicit-any + ? assign(create(), options, { [prop]: (props.format as any)[prop] }) // eslint-disable-line @typescript-eslint/no-explicit-any : options - }, {}) + }, create()) } const parts = partFormatter(...[props.value, options, overrides]) @@ -100,7 +100,7 @@ export function renderFormatter< children = [parts] } - const assignedAttrs = assign({}, attrs) + const assignedAttrs = assign(create(), attrs) const tag = isString(props.tag) || isObject(props.tag) ? props.tag
packages/vue-i18n-core/src/components/index.ts+4 −4 modified@@ -1,5 +1,5 @@ -export { ComponentI18nScope, BaseFormatProps } from './base' +export { BaseFormatProps, ComponentI18nScope } from './base' +export { DatetimeFormat, DatetimeFormatProps, I18nD } from './DatetimeFormat' export { FormattableProps } from './formatRenderer' -export { Translation, I18nT, TranslationProps } from './Translation' -export { NumberFormat, I18nN, NumberFormatProps } from './NumberFormat' -export { DatetimeFormat, I18nD, DatetimeFormatProps } from './DatetimeFormat' +export { I18nN, NumberFormat, NumberFormatProps } from './NumberFormat' +export { I18nT, Translation, TranslationProps } from './Translation'
packages/vue-i18n-core/src/components/NumberFormat.ts+5 −5 modified@@ -1,16 +1,16 @@ -import { defineComponent } from 'vue' -import { assign } from '@intlify/shared' import { NUMBER_FORMAT_OPTIONS_KEYS } from '@intlify/core-base' +import { assign } from '@intlify/shared' +import { defineComponent } from 'vue' import { useI18n } from '../i18n' import { NumberPartsSymbol } from '../symbols' -import { renderFormatter } from './formatRenderer' import { baseFormatProps } from './base' +import { renderFormatter } from './formatRenderer' -import type { VNodeProps } from 'vue' import type { NumberOptions } from '@intlify/core-base' +import type { VNodeProps } from 'vue' import type { Composer, ComposerInternal } from '../composer' -import type { FormattableProps } from './formatRenderer' import type { BaseFormatProps } from './base' +import type { FormattableProps } from './formatRenderer' /** * NumberFormat Component Props
packages/vue-i18n-core/src/components/Translation.ts+7 −7 modified@@ -1,12 +1,12 @@ -import { h, defineComponent } from 'vue' -import { isNumber, isString, isObject, assign } from '@intlify/shared' -import { TranslateVNodeSymbol } from '../symbols' +import { assign, create, isNumber, isObject, isString } from '@intlify/shared' +import { defineComponent, h } from 'vue' import { useI18n } from '../i18n' +import { TranslateVNodeSymbol } from '../symbols' import { baseFormatProps } from './base' -import { getInterpolateArg, getFragmentableTag } from './utils' +import { getFragmentableTag, getInterpolateArg } from './utils' -import type { VNodeChild, VNodeProps } from 'vue' import type { TranslateOptions } from '@intlify/core-base' +import type { VNodeChild, VNodeProps } from 'vue' import type { Composer, ComposerInternal } from '../composer' import type { BaseFormatProps } from './base' @@ -59,7 +59,7 @@ export const TranslationImpl = /*#__PURE__*/ defineComponent({ return (): VNodeChild => { const keys = Object.keys(slots).filter(key => key !== '_') - const options = {} as TranslateOptions + const options = create() as TranslateOptions if (props.locale) { options.locale = props.locale } @@ -73,7 +73,7 @@ export const TranslationImpl = /*#__PURE__*/ defineComponent({ arg, options ) - const assignedAttrs = assign({}, attrs) + const assignedAttrs = assign(create(), attrs) const tag = isString(props.tag) || isObject(props.tag) ? props.tag
packages/vue-i18n-core/src/components/utils.ts+2 −1 modified@@ -1,3 +1,4 @@ +import { create } from '@intlify/shared' import { Fragment } from 'vue' import type { NamedValue } from '@intlify/core-base' @@ -27,7 +28,7 @@ export function getInterpolateArg( arg[key] = slot() } return arg - }, {} as NamedValue) + }, create() as NamedValue) } }
packages/vue-i18n-core/src/index.ts+83 −83 modified@@ -1,120 +1,120 @@ -import { getGlobalThis } from '@intlify/shared' import { setDevToolsHook } from '@intlify/core-base' +import { getGlobalThis } from '@intlify/shared' import { initDev, initFeatureFlags } from './misc' export { - SchemaParams, - LocaleParams, - Path, - PathValue, - NamedValue, - Locale, + CompileError, + DateTimeOptions, FallbackLocale, - LocaleMessageValue, - LocaleMessageDictionary, - LocaleMessageType, - LocaleMessages, - LocaleMessage, - NumberFormat as IntlNumberFormat, DateTimeFormat as IntlDateTimeFormat, DateTimeFormats as IntlDateTimeFormats, - NumberFormats as IntlNumberFormats, - LocaleMatcher as IntlLocaleMatcher, FormatMatcher as IntlFormatMatcher, - MessageFunction, - MessageFunctions, - PluralizationRule, + LocaleMatcher as IntlLocaleMatcher, + NumberFormat as IntlNumberFormat, + NumberFormats as IntlNumberFormats, LinkedModifiers, - TranslateOptions, - DateTimeOptions, - NumberOptions, - PostTranslationHandler, - MessageResolver, + Locale, + LocaleMessage, + LocaleMessageDictionary, + LocaleMessages, + LocaleMessageType, + LocaleMessageValue, + LocaleParams, MessageCompiler, MessageCompilerContext, - CompileError, MessageContext, - RemovedIndexResources + MessageFunction, + MessageFunctions, + MessageResolver, + NamedValue, + NumberOptions, + Path, + PathValue, + PluralizationRule, + PostTranslationHandler, + RemovedIndexResources, + SchemaParams, + TranslateOptions } from '@intlify/core-base' export { - VueMessageType, - DefineLocaleMessage, - DefaultLocaleMessageSchema, - DefineDateTimeFormat, - DefaultDateTimeFormatSchema, - DefineNumberFormat, - DefaultNumberFormatSchema, - MissingHandler, - ComposerOptions, + BaseFormatProps, + ComponentI18nScope, + DatetimeFormat, + DatetimeFormatProps, + FormattableProps, + I18nD, + I18nN, + I18nT, + NumberFormat, + NumberFormatProps, + Translation, + TranslationProps +} from './components' +export { Composer, ComposerCustom, - CustomBlock, - CustomBlocks, - ComposerTranslation, ComposerDateTimeFormatting, ComposerNumberFormatting, - ComposerResolveLocaleMessageTranslation + ComposerOptions, + ComposerResolveLocaleMessageTranslation, + ComposerTranslation, + CustomBlock, + CustomBlocks, + DefaultDateTimeFormatSchema, + DefaultLocaleMessageSchema, + DefaultNumberFormatSchema, + DefineDateTimeFormat, + DefineLocaleMessage, + DefineNumberFormat, + MissingHandler, + VueMessageType } from './composer' export { - TranslateResult, + TranslationDirective, + vTDirective, + VTDirectiveValue +} from './directive' +export { + ComposerAdditionalOptions, + ComposerExtender, + createI18n, + ExportedGlobalComposer, + I18n, + I18nAdditionalOptions, + I18nInjectionKey, + I18nMode, + I18nOptions, + I18nScope, + useI18n, + UseI18nOptions +} from './i18n' +export { Choice, - LocaleMessageObject, - PluralizationRulesMap, - WarnHtmlInMessageLevel, DateTimeFormatResult, + LocaleMessageObject, NumberFormatResult, - VueI18nOptions, + PluralizationRulesMap, + TranslateResult, VueI18n, - VueI18nTranslation, - VueI18nTranslationChoice, VueI18nDateTimeFormatting, + VueI18nExtender, VueI18nNumberFormatting, + VueI18nOptions, VueI18nResolveLocaleMessageTranslation, - VueI18nExtender + VueI18nTranslation, + VueI18nTranslationChoice, + WarnHtmlInMessageLevel } from './legacy' -export { - createI18n, - useI18n, - I18nInjectionKey, - I18nOptions, - I18nAdditionalOptions, - I18n, - I18nMode, - I18nScope, - ComposerAdditionalOptions, - UseI18nOptions, - ExportedGlobalComposer, - ComposerExtender -} from './i18n' -export { - Translation, - I18nT, - NumberFormat, - I18nN, - DatetimeFormat, - I18nD, - TranslationProps, - NumberFormatProps, - DatetimeFormatProps, - FormattableProps, - BaseFormatProps, - ComponentI18nScope -} from './components' -export { - vTDirective, - VTDirectiveValue, - TranslationDirective -} from './directive' -export { I18nPluginOptions } from './plugin' export { VERSION } from './misc' +export { I18nPluginOptions } from './plugin' export { Disposer } from './types' export type { - IsNever, IsEmptyObject, - PickupPaths, + IsNever, + PickupFormatPathKeys, PickupKeys, - PickupFormatPathKeys + PickupPaths } from '@intlify/core-base' if (__ESM_BUNDLER__ && !__TEST__) {
packages/vue-i18n-core/src/utils.ts+9 −5 modified@@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { + create, deepCopy, hasOwn, isArray, @@ -69,7 +70,7 @@ export function handleFlatJson(obj: unknown): unknown { let hasStringValue = false for (let i = 0; i < lastIndex; i++) { if (!(subKeys[i] in currentObj)) { - currentObj[subKeys[i]] = {} + currentObj[subKeys[i]] = create() } if (!isObject(currentObj[subKeys[i]])) { __DEV__ && @@ -108,16 +109,16 @@ export function getLocaleMessages<Messages = {}>( const ret = (isPlainObject(messages) ? messages : isArray(__i18n) - ? {} - : { [locale]: {} }) as Record<string, any> + ? create() + : { [locale]: create() }) as Record<string, any> // merge locale messages of i18n custom block if (isArray(__i18n)) { __i18n.forEach(custom => { if ('locale' in custom && 'resource' in custom) { const { locale, resource } = custom if (locale) { - ret[locale] = ret[locale] || {} + ret[locale] = ret[locale] || create() deepCopy(resource, ret[locale]) } else { deepCopy(resource, ret) @@ -149,7 +150,10 @@ export function adjustI18nResources( options: ComposerOptions, componentOptions: any ): void { - let messages = isObject(options.messages) ? options.messages : {} + // prettier-ignore + let messages = isObject(options.messages) + ? options.messages + : create() as NonNullable<ComposerOptions['messages']> if ('__i18nGlobal' in componentOptions) { messages = getLocaleMessages(gl.locale.value as Locale, { messages,
packages/vue-i18n-core/src/warnings.ts+1 −1 modified@@ -1,5 +1,5 @@ -import { format } from '@intlify/shared' import { CORE_WARN_CODES_EXTEND_POINT } from '@intlify/core-base' +import { format } from '@intlify/shared' export const I18nWarnCodes = { FALLBACK_TO_ROOT: CORE_WARN_CODES_EXTEND_POINT, // 8
72f0d323006fMerge commit from fork
8 files changed · +839 −44
e2e/hotfix/CVE-2024-52809.html+69 −0 added@@ -0,0 +1,69 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8" /> + <title>vue-i18n XSS</title> + <script src="../../node_modules/vue/dist/vue.global.js"></script> + <script src="../../packages/vue-i18n/dist/vue-i18n.global.js"></script> + <!-- Scripts that perform prototype contamination, such as being distributed from malicious hosting sites or injected through supply chain attacks, etc. --> + <script> + /** + * Prototype pollution vulnerability with `Object.prototype`. + * The 'static' property is part of the optimized AST generated by the vue-i18n message compiler. + * About details of special properties, see https://github.com/intlify/vue-i18n/blob/master/packages/message-compiler/src/nodes.ts + * + * In general, the locale messages of vue-i18n are optimized during production builds using `@intlify/unplugin-vue-i18n`, + * so there is always a property that is attached during optimization like this time. + * But if you are using a locale message AST in development or your own, there is a possibility of XSS if a third party injects prototype pollution code. + */ + Object.defineProperty(Object.prototype, 'static', { + configurable: true, + get() { + alert('prototype polluted!') + return 'prototype pollution' + } + }) + </script> + </head> + <body> + <div id="app"> + <p>{{ t('hello') }}</p> + </div> + <script> + const { createApp } = Vue + const { createI18n, useI18n } = VueI18n + + // AST style locale message, which build by `@intlify/unplugin-vue-i18n` + const en = { + hello: { + type: 0, + body: { + items: [ + { + type: 3, + value: 'hello world!' + } + ] + } + } + } + + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en + } + }) + + const app = createApp({ + setup() { + const { t } = useI18n() + return { t } + } + }) + app.use(i18n) + app.mount('#app') + </script> + </body> +</html>
e2e/hotfix.spec.ts+11 −0 added@@ -0,0 +1,11 @@ +import { getText } from './helper' + +describe('CVE-2024-52809', () => { + beforeAll(async () => { + await page.goto(`http://localhost:8080/e2e/hotfix/CVE-2024-52809.html`) + }) + + test('fix', async () => { + expect(await getText(page, 'p')).toMatch('hello world!') + }) +})
packages/core-base/src/compilation.ts+17 −6 modified@@ -3,13 +3,21 @@ import { defaultOnError, detectHtmlTag } from '@intlify/message-compiler' -import { format, isBoolean, isObject, isString, warn } from '@intlify/shared' -import { format as formatMessage } from './format' +import { + format, + hasOwn, + isBoolean, + isObject, + isString, + warn +} from '@intlify/shared' +import { format as formatMessage, resolveType } from './format' import type { CompileError, CompileOptions, CompilerResult, + Node, ResourceNode } from '@intlify/message-compiler' import type { MessageCompilerContext } from './context' @@ -30,10 +38,13 @@ export function clearCompileCache(): void { compileCache = Object.create(null) } -export const isMessageAST = (val: unknown): val is ResourceNode => - isObject(val) && - (val.t === 0 || val.type === 0) && - ('b' in val || 'body' in val) +export function isMessageAST(val: unknown): val is ResourceNode { + return ( + isObject(val) && + resolveType(val as Node) === 0 && + (hasOwn(val, 'b') || hasOwn(val, 'body')) + ) +} function baseCompile( message: string,
packages/core-base/src/errors.ts+2 −2 modified@@ -1,6 +1,6 @@ import { - createCompileError, - COMPILE_ERROR_CODES_EXTEND_POINT + COMPILE_ERROR_CODES_EXTEND_POINT, + createCompileError } from '@intlify/message-compiler' import type { BaseError } from '@intlify/shared'
packages/core-base/src/format.ts+141 −33 modified@@ -1,23 +1,21 @@ import { NodeTypes } from '@intlify/message-compiler' +import { hasOwn, isNumber } from '@intlify/shared' import type { - Node, - TextNode, - LiteralNode, + LinkedModifierNode, + LinkedNode, ListNode, MessageNode, NamedNode, - LinkedNode, - LinkedKeyNode, - LinkedModifierNode, + Node, PluralNode, ResourceNode } from '@intlify/message-compiler' import type { MessageContext, MessageFunction, - MessageType, - MessageFunctionReturn + MessageFunctionReturn, + MessageType } from './runtime' export function format<Message = string>( @@ -28,14 +26,18 @@ export function format<Message = string>( return msg } -function formatParts<Message = string>( +export function formatParts<Message = string>( ctx: MessageContext<Message>, ast: ResourceNode ): MessageFunctionReturn<Message> { - const body = ast.b || ast.body - if ((body.t || body.type) === NodeTypes.Plural) { + const body = resolveBody(ast) + if (body == null) { + throw createUnhandleNodeError(NodeTypes.Resource) + } + const type = resolveType(body) + if (type === NodeTypes.Plural) { const plural = body as PluralNode - const cases = plural.c || plural.cases + const cases = resolveCases(plural) return ctx.plural( cases.reduce( (messages, c) => @@ -51,64 +53,170 @@ function formatParts<Message = string>( } } -function formatMessageParts<Message = string>( +const PROPS_BODY = ['b', 'body'] + +function resolveBody(node: ResourceNode) { + return resolveProps<MessageNode | PluralNode>(node, PROPS_BODY) +} + +const PROPS_CASES = ['c', 'cases'] + +function resolveCases(node: PluralNode) { + return resolveProps<PluralNode['cases'], PluralNode['cases']>( + node, + PROPS_CASES, + [] + ) +} + +export function formatMessageParts<Message = string>( ctx: MessageContext<Message>, node: MessageNode ): MessageFunctionReturn<Message> { - const _static = node.s || node.static - if (_static != null) { + const static_ = resolveStatic(node) + if (static_ != null) { return ctx.type === 'text' - ? (_static as MessageFunctionReturn<Message>) - : ctx.normalize([_static] as MessageType<Message>[]) + ? (static_ as MessageFunctionReturn<Message>) + : ctx.normalize([static_] as MessageType<Message>[]) } else { - const messages = (node.i || node.items).reduce( + const messages = resolveItems(node).reduce( (acm, c) => [...acm, formatMessagePart(ctx, c)], [] as MessageType<Message>[] ) return ctx.normalize(messages) as MessageFunctionReturn<Message> } } -function formatMessagePart<Message = string>( +const PROPS_STATIC = ['s', 'static'] + +function resolveStatic(node: MessageNode) { + return resolveProps(node, PROPS_STATIC) +} + +const PROPS_ITEMS = ['i', 'items'] + +function resolveItems(node: MessageNode) { + return resolveProps<MessageNode['items'], MessageNode['items']>( + node, + PROPS_ITEMS, + [] + ) +} + +type NodeValue<Message> = { + v?: MessageType<Message> + value?: MessageType<Message> +} + +export function formatMessagePart<Message = string>( ctx: MessageContext<Message>, node: Node ): MessageType<Message> { - const type = node.t || node.type + const type = resolveType(node) switch (type) { case NodeTypes.Text: { - const text = node as TextNode - return (text.v || text.value) as MessageType<Message> + return resolveValue<Message>(node as NodeValue<Message>, type) } case NodeTypes.Literal: { - const literal = node as LiteralNode - return (literal.v || literal.value) as MessageType<Message> + return resolveValue<Message>(node as NodeValue<Message>, type) } case NodeTypes.Named: { const named = node as NamedNode - return ctx.interpolate(ctx.named(named.k || named.key)) + if (hasOwn(named, 'k') && named.k) { + return ctx.interpolate(ctx.named(named.k)) + } + if (hasOwn(named, 'key') && named.key) { + return ctx.interpolate(ctx.named(named.key)) + } + throw createUnhandleNodeError(type) } case NodeTypes.List: { const list = node as ListNode - return ctx.interpolate(ctx.list(list.i != null ? list.i : list.index)) + if (hasOwn(list, 'i') && isNumber(list.i)) { + return ctx.interpolate(ctx.list(list.i)) + } + if (hasOwn(list, 'index') && isNumber(list.index)) { + return ctx.interpolate(ctx.list(list.index)) + } + throw createUnhandleNodeError(type) } case NodeTypes.Linked: { const linked = node as LinkedNode - const modifier = linked.m || linked.modifier + const modifier = resolveLinkedModifier(linked) + const key = resolveLinkedKey(linked) return ctx.linked( - formatMessagePart(ctx, linked.k || linked.key) as string, + formatMessagePart(ctx, key!) as string, modifier ? (formatMessagePart(ctx, modifier) as string) : undefined, ctx.type ) } case NodeTypes.LinkedKey: { - const linkedKey = node as LinkedKeyNode - return (linkedKey.v || linkedKey.value) as MessageType<Message> + return resolveValue<Message>(node as NodeValue<Message>, type) } case NodeTypes.LinkedModifier: { - const linkedModifier = node as LinkedModifierNode - return (linkedModifier.v || linkedModifier.value) as MessageType<Message> + return resolveValue<Message>(node as NodeValue<Message>, type) } default: - throw new Error(`unhandled node type on format message part: ${type}`) + throw new Error(`unhandled node on format message part: ${type}`) + } +} + +const PROPS_TYPE = ['t', 'type'] + +export function resolveType(node: Node) { + return resolveProps<NodeTypes>(node, PROPS_TYPE) +} + +const PROPS_VALUE = ['v', 'value'] + +function resolveValue<Message = string>( + node: { v?: MessageType<Message>; value?: MessageType<Message> }, + type: NodeTypes +): MessageType<Message> { + const resolved = resolveProps<Message>( + node as Node, + PROPS_VALUE + ) as MessageType<Message> + if (resolved) { + return resolved + } else { + throw createUnhandleNodeError(type) + } +} + +const PROPS_MODIFIER = ['m', 'modifier'] + +function resolveLinkedModifier(node: LinkedNode) { + return resolveProps<LinkedModifierNode>(node, PROPS_MODIFIER) +} + +const PROPS_KEY = ['k', 'key'] + +function resolveLinkedKey(node: LinkedNode) { + const resolved = resolveProps<LinkedNode['key']>(node, PROPS_KEY) + if (resolved) { + return resolved + } else { + throw createUnhandleNodeError(NodeTypes.Linked) } } + +function resolveProps<T = string, Default = undefined>( + node: Node, + props: string[], + defaultValue?: Default +): T | Default { + for (let i = 0; i < props.length; i++) { + const prop = props[i] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (hasOwn(node, prop) && (node as any)[prop] != null) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (node as any)[prop] as T + } + } + return defaultValue as Default +} + +function createUnhandleNodeError(type: NodeTypes) { + return new Error(`unhandled node type: ${type}`) +}
packages/core-base/test/format.test.ts+472 −2 modified@@ -1,7 +1,24 @@ -import { baseCompile as compile } from '@intlify/message-compiler' -import { format } from '../src/format' +import { baseCompile as compile, NodeTypes } from '@intlify/message-compiler' +import { + format, + formatMessagePart, + formatMessageParts, + formatParts +} from '../src/format' import { createMessageContext as context } from '../src/runtime' +import type { + LinkedKeyNode, + LinkedModifierNode, + LinkedNode, + ListNode, + LiteralNode, + MessageNode, + NamedNode, + ResourceNode, + TextNode +} from '@intlify/message-compiler' + describe('features', () => { test('text: hello world', () => { const { ast } = compile('hello world', { jit: true }) @@ -143,3 +160,456 @@ describe('edge cases', () => { expect(msg(ctx)).toBe('') }) }) + +describe('formatParts', () => { + test('prop: body', () => { + const node: ResourceNode = { + type: NodeTypes.Resource, + body: { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text, + value: 'hello world' + } + ] + } + } + + const ctx = context() + expect(formatParts(ctx, node)).toBe('hello world') + }) + + test('prop: b', () => { + const node: ResourceNode = { + type: NodeTypes.Resource, + body: { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text, + value: 'hello world' + } + ] + } + } + + const ctx = context() + expect(formatParts(ctx, node)).toBe('hello world') + }) + + test(`body has plural prop cases`, () => { + const node: ResourceNode = { + type: NodeTypes.Resource, + body: { + type: NodeTypes.Plural, + cases: [ + { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text, + value: 'hello' + } + ] + }, + { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text, + value: 'world' + } + ] + } + ] + } + } + + const ctx = context({ + pluralIndex: 2 + }) + expect(formatParts(ctx, node)).toBe('world') + }) + + test(`body has plural prop c`, () => { + const node: ResourceNode = { + type: NodeTypes.Resource, + // @ts-ignore + body: { + type: NodeTypes.Plural, + c: [ + { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text, + value: 'hello' + } + ] + }, + { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text, + value: 'world' + } + ] + } + ] + } + } + + const ctx = context({ + pluralIndex: 1 + }) + expect(formatParts(ctx, node)).toBe('hello') + }) + + test('not found prop body', () => { + // @ts-ignore + const node: ResourceNode = { + type: NodeTypes.Resource + } + + const ctx = context() + expect(() => formatParts(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.Resource}` + ) + }) +}) + +describe('formatMessageParts', () => { + test('prop: static', () => { + const node: MessageNode = { + type: NodeTypes.Message, + static: 'hello world', + items: [] + } + const ctx = context() + expect(formatMessageParts(ctx, node)).toBe('hello world') + }) + + test('prop: s', () => { + const node: MessageNode = { + type: NodeTypes.Message, + s: 'hello world', + items: [] + } + const ctx = context() + expect(formatMessageParts(ctx, node)).toBe('hello world') + }) + + test('prop: items', () => { + const node: MessageNode = { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text, + value: 'hello' + }, + { + type: NodeTypes.Text, + value: 'world' + } + ] + } + const ctx = context() + expect(formatMessageParts(ctx, node)).toEqual('helloworld') + }) + + test('prop: i', () => { + // @ts-ignore + const node: MessageNode = { + type: NodeTypes.Message, + i: [ + { + type: NodeTypes.Text, + value: 'hello' + }, + { + type: NodeTypes.Text, + value: 'world' + } + ] + } + const ctx = context() + expect(formatMessageParts(ctx, node)).toEqual('helloworld') + }) +}) + +describe('formatMessagePart', () => { + describe('text node', () => { + test('prop: value', () => { + const node: TextNode = { + type: NodeTypes.Text, + value: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test('prop: v', () => { + const node: TextNode = { + type: NodeTypes.Text, + v: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test(`prop 'value' and 'v' not found`, () => { + const node: TextNode = { + type: NodeTypes.Text + } + const ctx = context() + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.Text}` + ) + }) + }) + + describe('literal node', () => { + test('prop: value', () => { + const node: LiteralNode = { + type: NodeTypes.Literal, + value: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test('prop: v', () => { + const node: LiteralNode = { + type: NodeTypes.Literal, + v: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test(`prop 'value' and 'v' not found`, () => { + const node: LiteralNode = { + type: NodeTypes.Literal + } + const ctx = context() + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.Literal}` + ) + }) + }) + + describe('named node', () => { + test('prop: key', () => { + const node: NamedNode = { + type: NodeTypes.Named, + key: 'key' + } + const ctx = context({ + named: { key: 'hello world' } + }) + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test('prop: k', () => { + // @ts-ignore + const node: NamedNode = { + type: NodeTypes.Named, + k: 'key' + } + const ctx = context({ + named: { key: 'hello world' } + }) + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test(`prop 'key' and 'k' not found`, () => { + // @ts-ignore + const node: NamedNode = { + type: NodeTypes.Named + } + const ctx = context() + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.Named}` + ) + }) + }) + + describe('list node', () => { + test('prop: index', () => { + const node: ListNode = { + type: NodeTypes.List, + index: 0 + } + const ctx = context({ + list: ['hello world'] + }) + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test('prop: i', () => { + // @ts-ignore + const node: ListNode = { + type: NodeTypes.List, + i: 0 + } + const ctx = context({ + list: ['hello world'] + }) + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test(`prop 'index' and 'i' not found`, () => { + // @ts-ignore + const node: ListNode = { + type: NodeTypes.List + } + const ctx = context() + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.List}` + ) + }) + }) + + describe('linked key node', () => { + test('prop: value', () => { + const node: LinkedKeyNode = { + type: NodeTypes.LinkedKey, + value: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test('prop: v', () => { + // @ts-ignore + const node: LinkedKeyNode = { + type: NodeTypes.LinkedKey, + v: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test(`prop 'value' and 'v' not found`, () => { + // @ts-ignore + const node: LinkedKeyNode = { + type: NodeTypes.LinkedKey + } + const ctx = context() + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.LinkedKey}` + ) + }) + }) + + describe('linked modifier node', () => { + test('prop: value', () => { + const node: LinkedModifierNode = { + type: NodeTypes.LinkedModifier, + value: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test('prop: v', () => { + // @ts-ignore + const node: LinkedModifierNode = { + type: NodeTypes.LinkedModifier, + v: 'hello world' + } + const ctx = context() + expect(formatMessagePart(ctx, node)).toBe('hello world') + }) + + test(`prop 'value' and 'v' not found`, () => { + // @ts-ignore + const node: LinkedModifierNode = { + type: NodeTypes.LinkedModifier + } + const ctx = context() + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.LinkedModifier}` + ) + }) + }) + + describe('linked node', () => { + test('prop: modifier, key', () => { + const node: LinkedNode = { + type: NodeTypes.Linked, + modifier: { + type: NodeTypes.LinkedModifier, + value: 'upper' + }, + key: { + type: NodeTypes.LinkedKey, + value: 'name' + } + } + const ctx = context({ + modifiers: { + upper: (val: string) => val.toUpperCase() + }, + messages: { + name: () => 'kazupon' + } + }) + expect(formatMessagePart(ctx, node)).toBe('KAZUPON') + }) + + test('prop: m, k', () => { + // @ts-ignore + const node: LinkedNode = { + type: NodeTypes.Linked, + m: { + type: NodeTypes.LinkedModifier, + value: 'upper' + }, + k: { + type: NodeTypes.LinkedKey, + value: 'name' + } + } + const ctx = context({ + modifiers: { + upper: (val: string) => val.toUpperCase() + }, + messages: { + name: () => 'kazupon' + } + }) + expect(formatMessagePart(ctx, node)).toBe('KAZUPON') + }) + + test(`prop 'key' not found`, () => { + // @ts-ignore + const node: LinkedNode = { + type: NodeTypes.Linked + } + const ctx = context({ + modifiers: { + upper: (val: string) => val.toUpperCase() + }, + messages: { + name: () => 'kazupon' + } + }) + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node type: ${NodeTypes.Linked}` + ) + }) + }) + + test('unhandled node', () => { + const node = { + type: -1 + } + const ctx = context() + expect(() => formatMessagePart(ctx, node)).toThrow( + `unhandled node on format message part: -1` + ) + }) +})
packages/core-base/test/issues.test.ts+58 −0 added@@ -0,0 +1,58 @@ +import { format } from '../src/format' +import { createMessageContext as context } from '../src/runtime' + +import { NodeTypes, ResourceNode } from '@intlify/message-compiler' + +describe('CVE-2024-52809', () => { + function attackGetter() { + return 'polluted' + } + + afterEach(() => { + // @ts-ignore -- initialize polluted property + delete Object.prototype.static + }) + + test('success', () => { + Object.defineProperty(Object.prototype, 'static', { + configurable: true, + get: attackGetter + }) + const ast: ResourceNode = { + type: NodeTypes.Resource, + body: { + type: NodeTypes.Message, + static: 'hello world', + items: [ + { + type: NodeTypes.Text + } + ] + } + } + const msg = format(ast) + const ctx = context() + expect(msg(ctx)).toEqual('hello world') + }) + + test('error', () => { + Object.defineProperty(Object.prototype, 'static', { + configurable: true, + get: attackGetter + }) + const ast: ResourceNode = { + type: NodeTypes.Resource, + body: { + type: NodeTypes.Message, + items: [ + { + type: NodeTypes.Text + } + ] + } + } + const msg = format(ast) + const ctx = context() + expect(() => msg(ctx)).toThrow(`unhandled node type: ${NodeTypes.Text}`) + }) +})
packages/vue-i18n-core/test/issues.test.ts+69 −1 modified@@ -1406,7 +1406,7 @@ test('#1912', async () => { expect(el?.innerHTML).include(`No apples found`) }) -test('#1972', async () => { +test('#1972', () => { const i18n = createI18n({ legacy: false, locale: 'en', @@ -1418,3 +1418,71 @@ test('#1972', async () => { }) expect(i18n.global.t('test', 0)).toEqual('') }) + +describe('CVE-2024-52809', () => { + function attackGetter() { + return 'polluted' + } + + afterEach(() => { + // @ts-ignore -- initialize polluted property + delete Object.prototype.static + }) + + test('success', () => { + Object.defineProperty(Object.prototype, 'static', { + configurable: true, + get: attackGetter + }) + const en = { + hello: { + type: 0, + body: { + type: 2, + static: 'hello world', + items: [ + { + type: 3 + } + ] + } + } + } + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en + } + }) + expect(i18n.global.t('hello')).toEqual('hello world') + }) + + test('error', () => { + Object.defineProperty(Object.prototype, 'static', { + configurable: true, + get: attackGetter + }) + const en = { + hello: { + type: 0, + body: { + type: 2, + items: [ + { + type: 3 + } + ] + } + } + } + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en + } + }) + expect(() => i18n.global.t('hello')).toThrow(`unhandled node type: 3`) + }) +})
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-9r9m-ffp6-9x4vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-52809ghsaADVISORY
- github.com/intlify/vue-i18n/commit/72f0d323006fc7363b18cab62d4522dadd874411nvdWEB
- github.com/intlify/vue-i18n/commit/9f20909ef8c9232a1072d7818e12ed6d6451024dnvdWEB
- github.com/intlify/vue-i18n/security/advisories/GHSA-9r9m-ffp6-9x4vnvdWEB
News mentions
0No linked articles in our index yet.