Skip to content

Tag parsing fails with INVALID_TAG when used on site with MicrosoftAjax.js #7531

Description

@dkreft

Which package?

@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:

String.prototype.startsWith=function(a){return this.substr(0,a.length)===a}

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

  1. Run npm install intl-messageformat@12.1.2.
  2. Save the following as repro.mjs:
import { IntlMessageFormat } from 'intl-messageformat'

// Verbatim from MicrosoftAjax.js (ASP.NET ScriptManager)
String.prototype.startsWith = function(a){return this.substr(0,a.length)===a}

const message = 'Read our <link>Cookie Policy</link>.'

try {
  console.log(new IntlMessageFormat(message, 'en').format({
    link: (chunks) => `[${ chunks.join('') }]`,
  }))
} catch (e) {
  console.log(e.message)
}
  1. Run node repro.mjs. It prints INVALID_TAG.
  2. 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.
  • Version: @formatjs/icu-messageformat-parser 3.5.20

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:

if (this.message.slice(this.offset(), this.offset() + prefix.length) === prefix) {

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.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions