Skip to content

Commit b379535

Browse files
authored
fix: security vulnerability fix porting (#2034)
* chore: fix not work e2e in local (#2026) * chore: fix not work e2e in local * chore: update github actions and release for v10 * Merge commit from fork * fix: XSS vulnerability with prototype pollution on AST * test: add e2e test for scurity fix * fix: update e2e * fix: filename * fix: change type name * * fix: XSS vulnerability with prototype pollution on AST * fix: prototype pollusion on deepCopy * chore: revert workflow
1 parent aee7ef1 commit b379535

28 files changed

+1035
-144
lines changed

e2e/hotfix.spec.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getText } from './helper'
2+
3+
describe('CVE-2024-52809', () => {
4+
beforeAll(async () => {
5+
await page.goto(`http://localhost:8080/e2e/hotfix/CVE-2024-52809.html`)
6+
})
7+
8+
test('fix', async () => {
9+
expect(await getText(page, 'p')).toMatch('hello world!')
10+
})
11+
})

e2e/hotfix/CVE-2024-52809.html

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>vue-i18n XSS</title>
6+
<script src="../../node_modules/vue/dist/vue.global.js"></script>
7+
<script src="../../packages/vue-i18n/dist/vue-i18n.global.js"></script>
8+
<!-- Scripts that perform prototype contamination, such as being distributed from malicious hosting sites or injected through supply chain attacks, etc. -->
9+
<script>
10+
/**
11+
* Prototype pollution vulnerability with `Object.prototype`.
12+
* The 'static' property is part of the optimized AST generated by the vue-i18n message compiler.
13+
* About details of special properties, see https://github.com/intlify/vue-i18n/blob/master/packages/message-compiler/src/nodes.ts
14+
*
15+
* In general, the locale messages of vue-i18n are optimized during production builds using `@intlify/unplugin-vue-i18n`,
16+
* so there is always a property that is attached during optimization like this time.
17+
* 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.
18+
*/
19+
Object.defineProperty(Object.prototype, 'static', {
20+
configurable: true,
21+
get() {
22+
alert('prototype polluted!')
23+
return 'prototype pollution'
24+
}
25+
})
26+
</script>
27+
</head>
28+
<body>
29+
<div id="app">
30+
<p>{{ t('hello') }}</p>
31+
</div>
32+
<script>
33+
const { createApp } = Vue
34+
const { createI18n, useI18n } = VueI18n
35+
36+
// AST style locale message, which build by `@intlify/unplugin-vue-i18n`
37+
const en = {
38+
hello: {
39+
type: 0,
40+
body: {
41+
items: [
42+
{
43+
type: 3,
44+
value: 'hello world!'
45+
}
46+
]
47+
}
48+
}
49+
}
50+
51+
const i18n = createI18n({
52+
legacy: false,
53+
locale: 'en',
54+
messages: {
55+
en
56+
}
57+
})
58+
59+
const app = createApp({
60+
setup() {
61+
const { t } = useI18n()
62+
return { t }
63+
}
64+
})
65+
app.use(i18n)
66+
app.mount('#app')
67+
</script>
68+
</body>
69+
</html>

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@types/node": "^22.5.3",
9090
"@types/rc": "^1.2.4",
9191
"@vitest/coverage-v8": "^2.1.5",
92+
"@types/serve-handler": "^6.1.4",
9293
"api-docs-gen": "^0.4.0",
9394
"benchmark": "^2.1.4",
9495
"brotli": "^1.3.2",
@@ -121,7 +122,7 @@
121122
"rollup-plugin-node-globals": "^1.4.0",
122123
"rollup-plugin-typescript2": "^0.36.0",
123124
"secretlint": "^3.2.0",
124-
"serve-static": "^1.15.0",
125+
"serve-handler": "^6.1.6",
125126
"textlint": "^12.6.1",
126127
"textlint-filter-rule-comments": "^1.2.2",
127128
"textlint-rule-abbr-within-parentheses": "^1.0.2",

packages/core-base/src/compilation.ts

+20-8
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@ import {
33
defaultOnError,
44
detectHtmlTag
55
} from '@intlify/message-compiler'
6-
import { format, isBoolean, isObject, isString, warn } from '@intlify/shared'
7-
import { format as formatMessage } from './format'
6+
import {
7+
create,
8+
format,
9+
hasOwn,
10+
isBoolean,
11+
isObject,
12+
isString,
13+
warn
14+
} from '@intlify/shared'
15+
import { format as formatMessage, resolveType } from './format'
816

917
import type {
1018
CompileError,
1119
CompileOptions,
1220
CompilerResult,
21+
Node,
1322
ResourceNode
1423
} from '@intlify/message-compiler'
1524
import type { MessageCompilerContext } from './context'
@@ -24,16 +33,19 @@ function checkHtmlMessage(source: string, warnHtmlMessage?: boolean): void {
2433
}
2534

2635
const defaultOnCacheKey = (message: string): string => message
27-
let compileCache: unknown = Object.create(null)
36+
let compileCache: unknown = create()
2837

2938
export function clearCompileCache(): void {
30-
compileCache = Object.create(null)
39+
compileCache = create()
3140
}
3241

33-
export const isMessageAST = (val: unknown): val is ResourceNode =>
34-
isObject(val) &&
35-
(val.t === 0 || val.type === 0) &&
36-
('b' in val || 'body' in val)
42+
export function isMessageAST(val: unknown): val is ResourceNode {
43+
return (
44+
isObject(val) &&
45+
resolveType(val as Node) === 0 &&
46+
(hasOwn(val, 'b') || hasOwn(val, 'body'))
47+
)
48+
}
3749

3850
function baseCompile(
3951
message: string,

packages/core-base/src/context.ts

+11-8
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

+3-2
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/errors.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
2-
createCompileError,
3-
COMPILE_ERROR_CODES_EXTEND_POINT
2+
COMPILE_ERROR_CODES_EXTEND_POINT,
3+
createCompileError
44
} from '@intlify/message-compiler'
55

66
import type { BaseError } from '@intlify/shared'

0 commit comments

Comments
 (0)