Skip to content

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

Description

@ztcools

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

  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions