[Bug] I18n language validation via `in` lets ?lang=constructor break all Panel translations

Author: ztcoolsCreated Jul 30, 2026Updated Aug 3, 2026

What happened? / 问题描述

I18n validates the language argument with the in operator, which walks the prototype chain. Any Object.prototype key (constructor, toString, __proto__, valueOf, ...) therefore passes validation, and this.translations is assigned a function or Object.prototype instead of a locale object.

Every subsequent t() lookup then fails, so the Panel renders raw translation keys (ui.panel.ready instead of Ready) and emits a console.warn per call.

This is reachable from untrusted input: the demo entry reads lang from the URL query string and casts it with as, so there is no runtime validation.

https://github.com/alibaba/page-agent/blob/main/packages/page-agent/src/demo.ts#L35

typescript
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'

Expected: unknown values fall back to en-US, which already works for ordinary invalid input such as ?lang=fr-FR.

Actual: Object.prototype keys bypass the fallback and break every translation.

To be precise about severity: this is not prototype pollution — nothing is written to any prototype. It is a validation gap that silently degrades the UI.

Root cause

https://github.com/alibaba/page-agent/blob/main/packages/ui/src/i18n/index.ts#L14

typescript
this.language = language in locales ? language : 'en-US'

'constructor' in locales is true, while Object.hasOwn(locales, 'constructor') is false.

Suggested fix

typescript
this.language = Object.hasOwn(locales, language) ? language : 'en-US'

Object.hasOwn is ES2022 and the repo targets es2025 with lib: ["ESNext", "DOM"], so this needs no downlevel handling.

Related, same root cause

getNestedValue reduces with current?.[key], which also traverses the prototype chain, so t('toString') returns a Function even though the signature declares string:

https://github.com/alibaba/page-agent/blob/main/packages/ui/src/i18n/index.ts#L33

While investigating I also hit two unrelated edge cases in the same file — a TypeError when a key resolves to an intermediate object and params are passed, and empty-string translations being treated as missing by the if (!value) check. Those are separate bugs; I'd rather file them individually than mix them in here. Let me know if they're worth reporting.

How to reproduce / 如何复现

Verified against packages/ui/src/i18n/index.ts on main (b7401a0), Node 22.23.0.

Casts are needed because SupportedLanguage is a union type — which is exactly the point: the values below arrive as an unvalidated string at runtime.

typescript
import { I18n } from '@page-agent/ui'

new I18n('fr-FR' as any).t('ui.panel.ready')       // 'Ready'          ← fallback works
new I18n('constructor' as any).t('ui.panel.ready') // 'ui.panel.ready' ← broken
new I18n('__proto__' as any).t('ui.panel.ready')   // 'ui.panel.ready' ← broken
new I18n('toString' as any).t('ui.panel.ready')    // 'ui.panel.ready' ← broken

Observed output, including getLanguage() and the warning count:

?lang=en-US         getLanguage()=en-US         t()="Ready"            warns=0
?lang=zh-CN         getLanguage()=zh-CN         t()="准备就绪"          warns=0
?lang=fr-FR         getLanguage()=en-US         t()="Ready"            warns=0
?lang=constructor   getLanguage()=constructor   t()="ui.panel.ready"   warns=1
?lang=__proto__     getLanguage()=__proto__     t()="ui.panel.ready"   warns=1
?lang=toString      getLanguage()=toString      t()="ui.panel.ready"   warns=1

Note that getLanguage() also returns the bogus value rather than the applied fallback.

In a demo build the same thing is triggered purely from the URL, e.g. loading page-agent.demo.js?lang=constructor.

Version

1.12.2

Browser

Not browser-specific — reproduces in Node against the @page-agent/ui source.

Before submitting

  • I will be polite and respectful. / 我会保持礼貌与尊重。
  • I have read the Code of Conduct. / 我已阅读行为准则。
  • I have searched existing issues and this is not a duplicate.

Per #349 I'm asking before writing any code: would you like a PR for the one-line Object.hasOwn fix? packages/ui has no test setup yet, so a regression test would also need a vitest.config.js following the packages/llms template — happy to keep that out of scope and send the one-liner alone if you prefer.