Skip to content

Commit

Permalink
fix: security vulnerability fix porting (#2034)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kazupon authored Nov 28, 2024
1 parent aee7ef1 commit b379535
Show file tree
Hide file tree
Showing 28 changed files with 1,035 additions and 144 deletions.
11 changes: 11 additions & 0 deletions e2e/hotfix.spec.ts
Original file line number Diff line number Diff line change
@@ -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!')
})
})
69 changes: 69 additions & 0 deletions e2e/hotfix/CVE-2024-52809.html
Original file line number Diff line number Diff line change
@@ -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>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@types/node": "^22.5.3",
"@types/rc": "^1.2.4",
"@vitest/coverage-v8": "^2.1.5",
"@types/serve-handler": "^6.1.4",
"api-docs-gen": "^0.4.0",
"benchmark": "^2.1.4",
"brotli": "^1.3.2",
Expand Down Expand Up @@ -121,7 +122,7 @@
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-typescript2": "^0.36.0",
"secretlint": "^3.2.0",
"serve-static": "^1.15.0",
"serve-handler": "^6.1.6",
"textlint": "^12.6.1",
"textlint-filter-rule-comments": "^1.2.2",
"textlint-rule-abbr-within-parentheses": "^1.0.2",
Expand Down
28 changes: 20 additions & 8 deletions packages/core-base/src/compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ import {
defaultOnError,
detectHtmlTag
} from '@intlify/message-compiler'
import { format, isBoolean, isObject, isString, warn } from '@intlify/shared'
import { format as formatMessage } from './format'
import {
create,
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'
Expand All @@ -24,16 +33,19 @@ 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 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,
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
4 changes: 2 additions & 2 deletions packages/core-base/src/errors.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Loading

0 comments on commit b379535

Please sign in to comment.