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
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
this.language = language in locales ? language : 'en-US'
'constructor' in locales is true, while Object.hasOwn(locales, 'constructor') is false.
Suggested fix
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.
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
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.
What happened? / 问题描述
I18nvalidates the language argument with theinoperator, which walks the prototype chain.Any
Object.prototypekey (constructor,toString,__proto__,valueOf, ...) therefore passesvalidation, and
this.translationsis assigned a function orObject.prototypeinstead of a localeobject.
Every subsequent
t()lookup then fails, so the Panel renders raw translation keys(
ui.panel.readyinstead ofReady) and emits aconsole.warnper call.This is reachable from untrusted input: the demo entry reads
langfrom the URL query string andcasts it with
as, so there is no runtime validation.https://github.com/alibaba/page-agent/blob/main/packages/page-agent/src/demo.ts#L35
Expected: unknown values fall back to
en-US, which already works for ordinary invalid inputsuch as
?lang=fr-FR.Actual:
Object.prototypekeys 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
'constructor' in localesistrue, whileObject.hasOwn(locales, 'constructor')isfalse.Suggested fix
Object.hasOwnis ES2022 and the repo targetses2025withlib: ["ESNext", "DOM"], so this needsno downlevel handling.
Related, same root cause
getNestedValuereduces withcurrent?.[key], which also traverses the prototype chain, sot('toString')returns aFunctioneven though the signature declaresstring: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
TypeErrorwhen a keyresolves to an intermediate object and
paramsare passed, and empty-string translations beingtreated as missing by the
if (!value)check. Those are separate bugs; I'd rather file themindividually than mix them in here. Let me know if they're worth reporting.
How to reproduce / 如何复现
Verified against
packages/ui/src/i18n/index.tsonmain(b7401a0), Node 22.23.0.Casts are needed because
SupportedLanguageis a union type — which is exactly the point: thevalues below arrive as an unvalidated
stringat runtime.Observed output, including
getLanguage()and the warning count: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/uisource.Before submitting
Per #349 I'm asking before writing any code:
would you like a PR for the one-line
Object.hasOwnfix?packages/uihas no test setup yet, so aregression test would also need a
vitest.config.jsfollowing thepackages/llmstemplate — happyto keep that out of scope and send the one-liner alone if you prefer.