Severity: 🟢 Low (correctness + minor info-shape leak)
Location
src/i18n/translate.ts:49-54 (interpolate — if (key in params)); src/i18n/icu.ts:280 (arg node) and :284 (select node).
Description
Placeholder resolution uses the in operator, which walks the prototype chain, then String(params[key]). A placeholder whose name matches an Object.prototype member (toString, valueOf, constructor, hasOwnProperty, __proto__, …) is treated as "present" even when params is {}, and is substituted with the inherited value instead of being left as the literal {name}.
interpolate('Hello {toString}', {});
// => 'Hello function toString() { [native code] }'
// expected (documented contract): 'Hello {toString}'
Same for formatMessage('{x, select, other {{toString}}}', {}). Impact is limited (params is app-supplied and the placeholder must collide with a prototype member), so it is primarily a contract violation of "unmatched placeholders are left intact", with a minor info-shape leak (native function source) when a translation catalog is attacker/translator-controlled and the param is absent. Not a pollution or code-execution vector.
Suggested fix
Use an own-property check consistently at all three sites:
if (Object.prototype.hasOwnProperty.call(params, key)) { … }
Consider the same guard defensively in resolveKey (translate.ts:20-31), which currently traverses key segments without a prototype check (mitigated today only because messages are built with Object.create(null)).
Filed as part of a full-codebase security & correctness audit.
Severity: 🟢 Low (correctness + minor info-shape leak)
Location
src/i18n/translate.ts:49-54(interpolate—if (key in params));src/i18n/icu.ts:280(argnode) and:284(selectnode).Description
Placeholder resolution uses the
inoperator, which walks the prototype chain, thenString(params[key]). A placeholder whose name matches anObject.prototypemember (toString,valueOf,constructor,hasOwnProperty,__proto__, …) is treated as "present" even whenparamsis{}, and is substituted with the inherited value instead of being left as the literal{name}.Same for
formatMessage('{x, select, other {{toString}}}', {}). Impact is limited (paramsis app-supplied and the placeholder must collide with a prototype member), so it is primarily a contract violation of "unmatched placeholders are left intact", with a minor info-shape leak (native function source) when a translation catalog is attacker/translator-controlled and the param is absent. Not a pollution or code-execution vector.Suggested fix
Use an own-property check consistently at all three sites:
Consider the same guard defensively in
resolveKey(translate.ts:20-31), which currently traverses key segments without a prototype check (mitigated today only because messages are built withObject.create(null)).Filed as part of a full-codebase security & correctness audit.