You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@formatjs/icu-messageformat-parser 3.5.20, reached through intl-messageformat 12.1.2 and react-intl 12.1.3.
Describe the bug
ASP.NET Web Forms sites that use the ScriptManager control load MicrosoftAjax.js, served as ScriptResource.axd. That script replaces String.prototype.startsWith unconditionally, with a version that accepts only one argument:
bumpIf() calls this.message.startsWith(prefix, this.offset()) (parser.ts, line 1215). With the replacement in place, the position argument is ignored, so bumpIf() compares the prefix against the start of the whole message instead of the parser's current position. Any message containing a rich-text tag then fails with INVALID_TAG, and react-intl falls back to rendering the raw message, tags and all.
We embed a consent banner on our customers' sites, so we don't control what else runs on the page, and a good number of those customers run ASP.NET.
Important
The real culprit is Microsoft overwriting a built-in method. But MicrosoftAjax.js ships with every ScriptManager site, and neither we nor our customers can change what it does, and you can just imagine how willing Microsoft would be to change their library, so a fix in the parser is the only one that reaches those pages.
To Reproduce
Codesandbox URL
None. The reproduction below runs in Node, with no browser.
Reproducible Steps/Repo
Run npm install intl-messageformat@12.1.2.
Save the following as repro.mjs:
import{IntlMessageFormat}from'intl-messageformat'// Verbatim from MicrosoftAjax.js (ASP.NET ScriptManager)String.prototype.startsWith=function(a){returnthis.substr(0,a.length)===a}constmessage='Read our <link>Cookie Policy</link>.'try{console.log(newIntlMessageFormat(message,'en').format({link: (chunks)=>`[${chunks.join('')}]`,}))}catch(e){console.log(e.message)}
Run node repro.mjs. It prints INVALID_TAG.
Delete the String.prototype.startsWith line and run it again. It prints Read our [Cookie Policy].
Expected behavior
The message should format as Read our [Cookie Policy]. whether or not the page has replaced String.prototype.startsWith.
Screenshots
N/A
Desktop (please complete the following information):
OS: Any
Browser: Any, on a page that loads MicrosoftAjax.js. Also reproduced in Node 22.18.0.
Smartphone (please complete the following information):
Same as desktop.
Additional context
Older versions only hit this bug in one load order. In 2.11.4 and 3.0.0, the parser picked its startsWith() helper once, at module load, by testing '_a'.startsWith('a', 1). If the parser loaded after MicrosoftAjax.js, that test failed and the parser used its slice() fallback, so tags parsed correctly. If the parser loaded first, the test passed and it kept the native method, which MicrosoftAjax.js then replaced. Now that the fallback has been removed, the bug occurs in either order.
Replacing the startsWith() call in bumpIf() with a slice() comparison fixes it:
I made that change in the published 3.5.20 build, and the reproduction above printed Read our [Cookie Policy]. The parser's only other startsWith() call, styleAndLocation.style.startsWith('::'), doesn't pass a position, so the replacement doesn't affect it.
Which package?
@formatjs/icu-messageformat-parser3.5.20, reached throughintl-messageformat12.1.2 andreact-intl12.1.3.Describe the bug
ASP.NET Web Forms sites that use the ScriptManager control load
MicrosoftAjax.js, served asScriptResource.axd. That script replacesString.prototype.startsWithunconditionally, with a version that accepts only one argument:bumpIf()callsthis.message.startsWith(prefix, this.offset())(parser.ts, line 1215). With the replacement in place, the position argument is ignored, sobumpIf()compares the prefix against the start of the whole message instead of the parser's current position. Any message containing a rich-text tag then fails withINVALID_TAG, andreact-intlfalls back to rendering the raw message, tags and all.We embed a consent banner on our customers' sites, so we don't control what else runs on the page, and a good number of those customers run ASP.NET.
Important
The real culprit is Microsoft overwriting a built-in method. But
MicrosoftAjax.jsships with every ScriptManager site, and neither we nor our customers can change what it does, and you can just imagine how willing Microsoft would be to change their library, so a fix in the parser is the only one that reaches those pages.To Reproduce
Codesandbox URL
None. The reproduction below runs in Node, with no browser.
Reproducible Steps/Repo
npm install intl-messageformat@12.1.2.repro.mjs:node repro.mjs. It printsINVALID_TAG.String.prototype.startsWithline and run it again. It printsRead our [Cookie Policy].Expected behavior
The message should format as
Read our [Cookie Policy].whether or not the page has replacedString.prototype.startsWith.Screenshots
N/A
Desktop (please complete the following information):
MicrosoftAjax.js. Also reproduced in Node 22.18.0.@formatjs/icu-messageformat-parser3.5.20Smartphone (please complete the following information):
Same as desktop.
Additional context
Older versions only hit this bug in one load order. In 2.11.4 and 3.0.0, the parser picked its
startsWith()helper once, at module load, by testing'_a'.startsWith('a', 1). If the parser loaded afterMicrosoftAjax.js, that test failed and the parser used itsslice()fallback, so tags parsed correctly. If the parser loaded first, the test passed and it kept the native method, whichMicrosoftAjax.jsthen replaced. Now that the fallback has been removed, the bug occurs in either order.Replacing the
startsWith()call inbumpIf()with aslice()comparison fixes it:I made that change in the published 3.5.20 build, and the reproduction above printed
Read our [Cookie Policy].The parser's only otherstartsWith()call,styleAndLocation.style.startsWith('::'), doesn't pass a position, so the replacement doesn't affect it.