Skip to content

Commit

Permalink
Merge commit from fork
Browse files Browse the repository at this point in the history
* fix: XSS vulnerability with prototype pollution on AST

* test: add e2e test for scurity fix

* fix: prototype pollusion on deepCopy

* fix: update e2e

* fix: filename

* fix: change type name

* fix: more test case

* fix: change to `Object.create(null)` from object literal for more safety
  • Loading branch information
kazupon authored Nov 28, 2024
1 parent 72f0d32 commit 9f20909
Show file tree
Hide file tree
Showing 20 changed files with 210 additions and 159 deletions.
5 changes: 3 additions & 2 deletions packages/core-base/src/compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
detectHtmlTag
} from '@intlify/message-compiler'
import {
create,
format,
hasOwn,
isBoolean,
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 11 additions & 8 deletions packages/core-base/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
assign,
create,
isArray,
isBoolean,
isFunction,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions packages/core-base/src/datetime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
assign,
create,
isBoolean,
isDate,
isEmptyObject,
Expand Down Expand Up @@ -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)) {
Expand Down
29 changes: 15 additions & 14 deletions packages/core-base/src/number.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions packages/core-base/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HelperNameMap } from '@intlify/message-compiler'
import {
assign,
create,
isArray,
isFunction,
isNumber,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions packages/core-base/src/translate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
assign,
create,
escapeHtml,
generateCodeFrame,
generateFormatCacheKey,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) &&
Expand Down
9 changes: 5 additions & 4 deletions packages/message-compiler/src/tokenizer.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -447,6 +447,7 @@ export function createTokenizer(
function readText(scnr: Scanner): string {
let buf = ''


while (true) {
const ch = scnr.currentChar()
if (
Expand Down
7 changes: 5 additions & 2 deletions packages/shared/src/messages.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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])) {
Expand Down
5 changes: 4 additions & 1 deletion packages/shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -95,7 +98,7 @@ export const getGlobalThis = (): any => {
? window
: typeof global !== 'undefined'
? global
: {})
: create())
)
}

Expand Down
31 changes: 31 additions & 0 deletions packages/shared/test/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
10 changes: 5 additions & 5 deletions packages/vue-i18n-core/src/components/DatetimeFormat.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 5 additions & 5 deletions packages/vue-i18n-core/src/components/NumberFormat.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 7 additions & 7 deletions packages/vue-i18n-core/src/components/Translation.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/vue-i18n-core/src/components/base.ts
Original file line number Diff line number Diff line change
@@ -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'>

Expand Down
Loading

0 comments on commit 9f20909

Please sign in to comment.