Skip to content

Commit aabee0d

Browse files
authored
Merge commit from fork
* fix: prototype pollusion on deepCopy * fix: more test case * fix: change to `Object.create(null)` from object literal for more safety
1 parent b1331af commit aabee0d

File tree

13 files changed

+112
-63
lines changed

13 files changed

+112
-63
lines changed

packages/core-base/src/compilation.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isBoolean,
55
isObject,
66
isString,
7+
create,
78
warn
89
} from '@intlify/shared'
910
import { format as formatMessage, resolveType } from './format'
@@ -35,7 +36,7 @@ function checkHtmlMessage(source: string, warnHtmlMessage?: boolean): void {
3536
}
3637

3738
const defaultOnCacheKey = (message: string): string => message
38-
let compileCache: unknown = Object.create(null)
39+
let compileCache: unknown = create()
3940

4041
function onCompileWarn(_warn: CompileWarn): void {
4142
if (_warn.code === CompileWarnCodes.USE_MODULO_SYNTAX) {
@@ -49,7 +50,7 @@ function onCompileWarn(_warn: CompileWarn): void {
4950
}
5051

5152
export function clearCompileCache(): void {
52-
compileCache = Object.create(null)
53+
compileCache = create()
5354
}
5455

5556
export function isMessageAST(val: unknown): val is ResourceNode {

packages/core-base/src/context.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isFunction,
1010
isPlainObject,
1111
assign,
12+
create,
1213
isObject,
1314
warnOnce
1415
} from '@intlify/shared'
@@ -510,23 +511,23 @@ export function createCoreContext<Message = string>(options: any = {}): any {
510511
: _locale
511512
const messages = isPlainObject(options.messages)
512513
? options.messages
513-
: { [_locale]: {} }
514+
: createResources(_locale)
514515
const datetimeFormats = !__LITE__
515516
? isPlainObject(options.datetimeFormats)
516517
? options.datetimeFormats
517-
: { [_locale]: {} }
518-
: /* #__PURE__*/ { [_locale]: {} }
518+
: createResources(_locale)
519+
: /* #__PURE__*/ createResources(_locale)
519520
const numberFormats = !__LITE__
520521
? isPlainObject(options.numberFormats)
521522
? options.numberFormats
522-
: { [_locale]: {} }
523-
: /* #__PURE__*/ { [_locale]: {} }
523+
: createResources(_locale)
524+
: /* #__PURE__*/ createResources(_locale)
524525
const modifiers = assign(
525-
{},
526-
options.modifiers || {},
526+
create(),
527+
options.modifiers,
527528
getDefaultLinkedModifiers<Message>()
528529
)
529-
const pluralRules = options.pluralRules || {}
530+
const pluralRules = options.pluralRules || create()
530531
const missing = isFunction(options.missing) ? options.missing : null
531532
const missingWarn =
532533
isBoolean(options.missingWarn) || isRegExp(options.missingWarn)
@@ -631,6 +632,8 @@ export function createCoreContext<Message = string>(options: any = {}): any {
631632
return context
632633
}
633634

635+
const createResources = (locale: Locale) => ({ [locale]: create() })
636+
634637
/** @internal */
635638
export function isTranslateFallbackWarn(
636639
fallback: boolean | RegExp,

packages/core-base/src/datetime.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isDate,
66
isNumber,
77
isEmptyObject,
8+
create,
89
assign
910
} from '@intlify/shared'
1011
import {
@@ -323,8 +324,8 @@ export function parseDateTimeArgs(
323324
...args: unknown[]
324325
): [string, number | Date, DateTimeOptions, Intl.DateTimeFormatOptions] {
325326
const [arg1, arg2, arg3, arg4] = args
326-
const options = {} as DateTimeOptions
327-
let overrides = {} as Intl.DateTimeFormatOptions
327+
const options = create() as DateTimeOptions
328+
let overrides = create() as Intl.DateTimeFormatOptions
328329

329330
let value: number | Date
330331
if (isString(arg1)) {

packages/core-base/src/number.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isPlainObject,
55
isNumber,
66
isEmptyObject,
7+
create,
78
assign
89
} from '@intlify/shared'
910
import {
@@ -318,8 +319,8 @@ export function parseNumberArgs(
318319
...args: unknown[]
319320
): [string, number, NumberOptions, Intl.NumberFormatOptions] {
320321
const [arg1, arg2, arg3, arg4] = args
321-
const options = {} as NumberOptions
322-
let overrides = {} as Intl.NumberFormatOptions
322+
const options = create() as NumberOptions
323+
let overrides = create() as Intl.NumberFormatOptions
323324

324325
if (!isNumber(arg1)) {
325326
throw createCoreError(CoreErrorCodes.INVALID_ARGUMENT)

packages/core-base/src/runtime.ts

+23-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
assign,
3+
create,
34
isNumber,
45
isFunction,
56
toDisplayString,
@@ -16,10 +17,10 @@ type ExtractToStringKey<T> = Extract<keyof T, 'toString'>
1617
type ExtractToStringFunction<T> = T[ExtractToStringKey<T>]
1718
// prettier-ignore
1819
type StringConvertable<T> = ExtractToStringKey<T> extends never
19-
? unknown
20-
: ExtractToStringFunction<T> extends (...args: any) => string // eslint-disable-line @typescript-eslint/no-explicit-any
21-
? T
22-
: unknown
20+
? unknown
21+
: ExtractToStringFunction<T> extends (...args: any) => string // eslint-disable-line @typescript-eslint/no-explicit-any
22+
? T
23+
: unknown
2324

2425
/** @VueI18nGeneral */
2526
export type Locale = string
@@ -279,27 +280,27 @@ function pluralDefault(choice: number, choicesLength: number): number {
279280
if (choicesLength === 2) {
280281
// prettier-ignore
281282
return choice
282-
? choice > 1
283-
? 1
284-
: 0
285-
: 1
283+
? choice > 1
284+
? 1
285+
: 0
286+
: 1
286287
}
287288
return choice ? Math.min(choice, 2) : 0
288289
}
289290

290291
function getPluralIndex<T, N>(options: MessageContextOptions<T, N>): number {
291292
// prettier-ignore
292293
const index = isNumber(options.pluralIndex)
293-
? options.pluralIndex
294-
: -1
294+
? options.pluralIndex
295+
: -1
295296
// prettier-ignore
296297
return options.named && (isNumber(options.named.count) || isNumber(options.named.n))
297-
? isNumber(options.named.count)
298-
? options.named.count
299-
: isNumber(options.named.n)
300-
? options.named.n
301-
: index
302-
: index
298+
? isNumber(options.named.count)
299+
? options.named.count
300+
: isNumber(options.named.n)
301+
? options.named.n
302+
: index
303+
: index
303304
}
304305

305306
function normalizeNamed(pluralIndex: number, props: PluralizationProps): void {
@@ -336,17 +337,17 @@ export function createMessageContext<T = string, N = {}>(
336337
const list = (index: number): unknown => _list[index]
337338

338339
// eslint-disable-next-line @typescript-eslint/no-explicit-any
339-
const _named = options.named || ({} as any)
340+
const _named = options.named || (create() as any)
340341
isNumber(options.pluralIndex) && normalizeNamed(pluralIndex, _named)
341342
const named = (key: string): unknown => _named[key]
342343

343344
function message(key: Path): MessageFunction<T> {
344345
// prettier-ignore
345346
const msg = isFunction(options.messages)
346-
? options.messages(key)
347-
: isObject(options.messages)
348-
? options.messages[key]
349-
: false
347+
? options.messages(key)
348+
: isObject(options.messages)
349+
? options.messages[key]
350+
: false
350351
return !msg
351352
? options.parent
352353
? options.parent.message(key) // resolve from parent messages
@@ -412,7 +413,7 @@ export function createMessageContext<T = string, N = {}>(
412413
[HelperNameMap.TYPE]: type,
413414
[HelperNameMap.INTERPOLATE]: interpolate,
414415
[HelperNameMap.NORMALIZE]: normalize,
415-
[HelperNameMap.VALUES]: assign({}, _list, _named)
416+
[HelperNameMap.VALUES]: assign(create(), _list, _named)
416417
}
417418

418419
return ctx

packages/core-base/src/translate.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
mark,
1515
measure,
1616
assign,
17+
create,
1718
isObject
1819
} from '@intlify/shared'
1920
import { isMessageAST } from './compilation'
@@ -675,7 +676,7 @@ export function translate<
675676
: [
676677
key,
677678
locale,
678-
(messages as unknown as LocaleMessages<Message>)[locale] || {}
679+
(messages as unknown as LocaleMessages<Message>)[locale] || create()
679680
]
680681
// NOTE:
681682
// Fix to work around `ssrTransfrom` bug in Vite.
@@ -777,14 +778,14 @@ export function translate<
777778
? (format as MessageFunctionInternal).key!
778779
: '',
779780
locale: targetLocale || (isMessageFunction(format)
780-
? (format as MessageFunctionInternal).locale!
781-
: ''),
781+
? (format as MessageFunctionInternal).locale!
782+
: ''),
782783
format:
783784
isString(format)
784-
? format
785-
: isMessageFunction(format)
786-
? (format as MessageFunctionInternal).source!
787-
: '',
785+
? format
786+
: isMessageFunction(format)
787+
? (format as MessageFunctionInternal).source!
788+
: '',
788789
message: ret as string
789790
}
790791
;(payloads as AdditionalPayloads).meta = assign(
@@ -828,7 +829,7 @@ function resolveMessageFormat<Messages, Message>(
828829
} = context
829830
const locales = localeFallbacker(context as any, fallbackLocale, locale) // eslint-disable-line @typescript-eslint/no-explicit-any
830831

831-
let message: LocaleMessageValue<Message> = {}
832+
let message: LocaleMessageValue<Message> = create()
832833
let targetLocale: Locale | undefined
833834
let format: PathValue = null
834835
let from: Locale = locale
@@ -867,7 +868,7 @@ function resolveMessageFormat<Messages, Message>(
867868
}
868869

869870
message =
870-
(messages as unknown as LocaleMessages<Message>)[targetLocale] || {}
871+
(messages as unknown as LocaleMessages<Message>)[targetLocale] || create()
871872

872873
// for vue-devtools timeline event
873874
let start: number | null = null
@@ -1042,7 +1043,7 @@ export function parseTranslateArgs<Message = string>(
10421043
...args: unknown[]
10431044
): [Path | MessageFunction<Message> | ResourceNode, TranslateOptions] {
10441045
const [arg1, arg2, arg3] = args
1045-
const options = {} as TranslateOptions
1046+
const options = create() as TranslateOptions
10461047

10471048
if (
10481049
!isString(arg1) &&

packages/shared/src/messages.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isArray, isObject } from './utils'
1+
import { isArray, isObject, create } from './utils'
22

33
const isNotObjectOrIsArray = (val: unknown) => !isObject(val) || isArray(val)
44
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
@@ -14,10 +14,13 @@ export function deepCopy(src: any, des: any): void {
1414

1515
// using `Object.keys` which skips prototype properties
1616
Object.keys(src).forEach(key => {
17+
if (key === '__proto__') {
18+
return
19+
}
1720
// if src[key] is an object/array, set des[key]
1821
// to empty object/array to prevent setting by reference
1922
if (isObject(src[key]) && !isObject(des[key])) {
20-
des[key] = Array.isArray(src[key]) ? [] : {}
23+
des[key] = Array.isArray(src[key]) ? [] : create()
2124
}
2225

2326
if (isNotObjectOrIsArray(des[key]) || isNotObjectOrIsArray(src[key])) {

packages/shared/src/utils.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export const isEmptyObject = (val: unknown): val is boolean =>
8181

8282
export const assign = Object.assign
8383

84+
const _create = Object.create
85+
export const create = (obj: object | null = null): object => _create(obj)
86+
8487
let _globalThis: any
8588
export const getGlobalThis = (): any => {
8689
// prettier-ignore
@@ -95,7 +98,7 @@ export const getGlobalThis = (): any => {
9598
? window
9699
: typeof global !== 'undefined'
97100
? global
98-
: {})
101+
: create())
99102
)
100103
}
101104

packages/shared/test/messages.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,34 @@ test('deepCopy merges without mutating src argument', () => {
4848
// should not mutate source object
4949
expect(msg1).toStrictEqual(copy1)
5050
})
51+
52+
describe('CVE-2024-52810', () => {
53+
test('__proto__', () => {
54+
const source = '{ "__proto__": { "pollutedKey": 123 } }'
55+
const dest = {}
56+
57+
deepCopy(JSON.parse(source), dest)
58+
expect(dest).toEqual({})
59+
// @ts-ignore -- initialize polluted property
60+
expect(JSON.parse(JSON.stringify({}.__proto__))).toEqual({})
61+
})
62+
63+
test('nest __proto__', () => {
64+
const source = '{ "foo": { "__proto__": { "pollutedKey": 123 } } }'
65+
const dest = {}
66+
67+
deepCopy(JSON.parse(source), dest)
68+
expect(dest).toEqual({ foo: {} })
69+
// @ts-ignore -- initialize polluted property
70+
expect(JSON.parse(JSON.stringify({}.__proto__))).toEqual({})
71+
})
72+
73+
test('constructor prototype', () => {
74+
const source = '{ "constructor": { "prototype": { "polluted": 1 } } }'
75+
const dest = {}
76+
77+
deepCopy(JSON.parse(source), dest)
78+
// @ts-ignore -- initialize polluted property
79+
expect({}.polluted).toBeUndefined()
80+
})
81+
})

packages/vue-i18n-core/src/components/Translation.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { h, defineComponent } from 'vue'
2-
import { isNumber, isString, isObject, assign } from '@intlify/shared'
2+
import { isNumber, isString, isObject, assign, create } from '@intlify/shared'
33
import { TranslateVNodeSymbol } from '../symbols'
44
import { useI18n } from '../i18n'
55
import { baseFormatProps } from './base'
@@ -59,7 +59,7 @@ export const TranslationImpl = /*#__PURE__*/ defineComponent({
5959

6060
return (): VNodeChild => {
6161
const keys = Object.keys(slots).filter(key => key !== '_')
62-
const options = {} as TranslateOptions
62+
const options = create() as TranslateOptions
6363
if (props.locale) {
6464
options.locale = props.locale
6565
}
@@ -73,7 +73,7 @@ export const TranslationImpl = /*#__PURE__*/ defineComponent({
7373
arg,
7474
options
7575
)
76-
const assignedAttrs = assign({}, attrs)
76+
const assignedAttrs = assign(create(), attrs)
7777
const tag =
7878
isString(props.tag) || isObject(props.tag)
7979
? props.tag

0 commit comments

Comments
 (0)