Skip to content

Commit 9f20909

Browse files
authored
Merge commit from fork
* 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
1 parent 72f0d32 commit 9f20909

File tree

20 files changed

+210
-159
lines changed

20 files changed

+210
-159
lines changed

packages/core-base/src/compilation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
detectHtmlTag
55
} from '@intlify/message-compiler'
66
import {
7+
create,
78
format,
89
hasOwn,
910
isBoolean,
@@ -32,10 +33,10 @@ function checkHtmlMessage(source: string, warnHtmlMessage?: boolean): void {
3233
}
3334

3435
const defaultOnCacheKey = (message: string): string => message
35-
let compileCache: unknown = Object.create(null)
36+
let compileCache: unknown = create()
3637

3738
export function clearCompileCache(): void {
38-
compileCache = Object.create(null)
39+
compileCache = create()
3940
}
4041

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

packages/core-base/src/context.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import {
44
assign,
5+
create,
56
isArray,
67
isBoolean,
78
isFunction,
@@ -507,23 +508,23 @@ export function createCoreContext<Message = string>(options: any = {}): any {
507508
: _locale
508509
const messages = isPlainObject(options.messages)
509510
? options.messages
510-
: { [_locale]: {} }
511+
: createResources(_locale)
511512
const datetimeFormats = !__LITE__
512513
? isPlainObject(options.datetimeFormats)
513514
? options.datetimeFormats
514-
: { [_locale]: {} }
515-
: { [_locale]: {} }
515+
: createResources(_locale)
516+
: createResources(_locale)
516517
const numberFormats = !__LITE__
517518
? isPlainObject(options.numberFormats)
518519
? options.numberFormats
519-
: { [_locale]: {} }
520-
: { [_locale]: {} }
520+
: createResources(_locale)
521+
: createResources(_locale)
521522
const modifiers = assign(
522-
{},
523-
options.modifiers || {},
523+
create(),
524+
options.modifiers,
524525
getDefaultLinkedModifiers<Message>()
525526
)
526-
const pluralRules = options.pluralRules || {}
527+
const pluralRules = options.pluralRules || create()
527528
const missing = isFunction(options.missing) ? options.missing : null
528529
const missingWarn =
529530
isBoolean(options.missingWarn) || isRegExp(options.missingWarn)
@@ -628,6 +629,8 @@ export function createCoreContext<Message = string>(options: any = {}): any {
628629
return context
629630
}
630631

632+
const createResources = (locale: Locale) => ({ [locale]: create() })
633+
631634
/** @internal */
632635
export function isTranslateFallbackWarn(
633636
fallback: boolean | RegExp,

packages/core-base/src/datetime.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
assign,
3+
create,
34
isBoolean,
45
isDate,
56
isEmptyObject,
@@ -322,8 +323,8 @@ export function parseDateTimeArgs(
322323
...args: unknown[]
323324
): [string, number | Date, DateTimeOptions, Intl.DateTimeFormatOptions] {
324325
const [arg1, arg2, arg3, arg4] = args
325-
const options = {} as DateTimeOptions
326-
let overrides = {} as Intl.DateTimeFormatOptions
326+
const options = create() as DateTimeOptions
327+
let overrides = create() as Intl.DateTimeFormatOptions
327328

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

packages/core-base/src/number.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
import {
2-
isString,
2+
assign,
3+
create,
34
isBoolean,
4-
isPlainObject,
5-
isNumber,
65
isEmptyObject,
7-
assign
6+
isNumber,
7+
isPlainObject,
8+
isString
89
} from '@intlify/shared'
910
import {
1011
handleMissing,
1112
isTranslateFallbackWarn,
12-
NOT_REOSLVED,
13-
MISSING_RESOLVE_VALUE
13+
MISSING_RESOLVE_VALUE,
14+
NOT_REOSLVED
1415
} from './context'
15-
import { CoreWarnCodes, getWarnMessage } from './warnings'
1616
import { CoreErrorCodes, createCoreError } from './errors'
17-
import { Availabilities } from './intl'
1817
import { getLocale } from './fallbacker'
18+
import { Availabilities } from './intl'
19+
import { CoreWarnCodes, getWarnMessage } from './warnings'
1920

20-
import type { Locale, FallbackLocale } from './runtime'
21+
import type { CoreContext, CoreInternalContext } from './context'
22+
import type { LocaleOptions } from './fallbacker'
23+
import type { FallbackLocale, Locale } from './runtime'
2124
import type {
2225
NumberFormat,
23-
NumberFormats as NumberFormatsType,
2426
NumberFormatOptions,
27+
NumberFormats as NumberFormatsType,
2528
PickupFormatKeys
2629
} from './types'
27-
import type { LocaleOptions } from './fallbacker'
28-
import type { CoreContext, CoreInternalContext } from './context'
2930

3031
/**
3132
* # number
@@ -317,8 +318,8 @@ export function parseNumberArgs(
317318
...args: unknown[]
318319
): [string, number, NumberOptions, Intl.NumberFormatOptions] {
319320
const [arg1, arg2, arg3, arg4] = args
320-
const options = {} as NumberOptions
321-
let overrides = {} as Intl.NumberFormatOptions
321+
const options = create() as NumberOptions
322+
let overrides = create() as Intl.NumberFormatOptions
322323

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

packages/core-base/src/runtime.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { HelperNameMap } from '@intlify/message-compiler'
22
import {
33
assign,
4+
create,
45
isArray,
56
isFunction,
67
isNumber,
@@ -360,7 +361,7 @@ export function createMessageContext<T = string, N = {}>(
360361
const list = (index: number): unknown => _list[index]
361362

362363
// eslint-disable-next-line @typescript-eslint/no-explicit-any
363-
const _named = options.named || ({} as any)
364+
const _named = options.named || (create() as any)
364365

365366
isNumber(options.pluralIndex) && normalizeNamed(pluralIndex, _named)
366367
const named = (key: string): unknown => _named[key]
@@ -437,7 +438,7 @@ export function createMessageContext<T = string, N = {}>(
437438
[HelperNameMap.TYPE]: type,
438439
[HelperNameMap.INTERPOLATE]: interpolate,
439440
[HelperNameMap.NORMALIZE]: normalize,
440-
[HelperNameMap.VALUES]: assign({}, _list, _named)
441+
[HelperNameMap.VALUES]: assign(create(), _list, _named)
441442
}
442443

443444
return ctx

packages/core-base/src/translate.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
assign,
3+
create,
34
escapeHtml,
45
generateCodeFrame,
56
generateFormatCacheKey,
@@ -677,7 +678,7 @@ export function translate<
677678
: [
678679
key,
679680
locale,
680-
(messages as unknown as LocaleMessages<Message>)[locale] || {}
681+
(messages as unknown as LocaleMessages<Message>)[locale] || create()
681682
]
682683
// NOTE:
683684
// Fix to work around `ssrTransfrom` bug in Vite.
@@ -830,7 +831,7 @@ function resolveMessageFormat<Messages, Message>(
830831
} = context
831832
const locales = localeFallbacker(context as any, fallbackLocale, locale) // eslint-disable-line @typescript-eslint/no-explicit-any
832833

833-
let message: LocaleMessageValue<Message> = {}
834+
let message: LocaleMessageValue<Message> = create()
834835
let targetLocale: Locale | undefined
835836
let format: PathValue = null
836837
let from: Locale = locale
@@ -869,7 +870,7 @@ function resolveMessageFormat<Messages, Message>(
869870
}
870871

871872
message =
872-
(messages as unknown as LocaleMessages<Message>)[targetLocale] || {}
873+
(messages as unknown as LocaleMessages<Message>)[targetLocale] || create()
873874

874875
// for vue-devtools timeline event
875876
let start: number | null = null
@@ -1044,7 +1045,7 @@ export function parseTranslateArgs<Message = string>(
10441045
...args: unknown[]
10451046
): [Path | MessageFunction<Message> | ResourceNode, TranslateOptions] {
10461047
const [arg1, arg2, arg3] = args
1047-
const options = {} as TranslateOptions
1048+
const options = create() as TranslateOptions
10481049

10491050
if (
10501051
!isString(arg1) &&

packages/message-compiler/src/tokenizer.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { createScanner, CHAR_SP as SPACE, CHAR_LF as NEW_LINE } from './scanner'
1+
import { CompileErrorCodes, createCompileError } from './errors'
22
import { createLocation, createPosition } from './location'
3-
import { createCompileError, CompileErrorCodes } from './errors'
3+
import { createScanner, CHAR_LF as NEW_LINE, CHAR_SP as SPACE } from './scanner'
44

5-
import type { Scanner } from './scanner'
6-
import type { SourceLocation, Position } from './location'
5+
import type { Position, SourceLocation } from './location'
76
import type { TokenizeOptions } from './options'
7+
import type { Scanner } from './scanner'
88

99
export const enum TokenTypes {
1010
Text, // 0
@@ -447,6 +447,7 @@ export function createTokenizer(
447447
function readText(scnr: Scanner): string {
448448
let buf = ''
449449

450+
450451
while (true) {
451452
const ch = scnr.currentChar()
452453
if (

packages/shared/src/messages.ts

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

33
const isNotObjectOrIsArray = (val: unknown) => !isObject(val) || isArray(val)
44
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 31 additions & 0 deletions
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+
})

0 commit comments

Comments
 (0)