VYPR
Medium severityOSV Advisory· Published Nov 29, 2024· Updated Apr 15, 2026

CVE-2024-52809

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.

PackageAffected versionsPatched versions
@intlify/core-basenpm
>= 9.3.0, < 9.14.29.14.2
vue-i18nnpm
>= 9.3.0, < 9.14.29.14.2
@intlify/corenpm
>= 9.3.0, < 9.14.29.14.2
@intlify/vue-i18n-corenpm
>= 9.3.0, < 9.14.29.14.2
petite-vue-i18nnpm
>= 10.0.0, < 10.0.510.0.5
@intlify/core-basenpm
>= 10.0.0, < 10.0.510.0.5
vue-i18nnpm
>= 10.0.0, < 10.0.510.0.5
@intlify/corenpm
>= 10.0.0, < 10.0.510.0.5
@intlify/vue-i18n-corenpm
>= 10.0.0, < 10.0.510.0.5

Affected products

7

Patches

4
9f20909ef8c9

Merge commit from fork

https://github.com/intlify/vue-i18nkazuya kawaguchiNov 28, 2024via ghsa
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
    
72f0d323006f

Merge commit from fork

https://github.com/intlify/vue-i18nkazuya kawaguchiNov 28, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.