The map library MapLibre GL JS has a CVSS 10.0 (CRITICAL) cross-site scripting flaw, CVE-2026-85061. The npm package `maplibre-gl` is affected in 6.4.0 and earlier, and the fix landed in 6.4.1.
What makes this one worth your attention is that the bug is in the sanitizer itself. The routine that is supposed to strip dangerous attributes misses one, and the survivor executes as soon as the map renders its attribution control. No click, no hover, no user action of any kind.
What to do right now
- ✓Update the `maplibre-gl` npm package to 6.4.1 or later. As of September 4, 2026 the newest release is 6.7.0, published September 2, 2026
- ✓Run `npm ls maplibre-gl` to see which version you actually have and where it comes from. Internal dashboards and admin tools often pull it in as a transitive dependency
- ✓If you cannot upgrade immediately, apply the workaround the maintainers published: sanitize a source's `attribution` field before passing it to MapLibre
- ✓Check whether you load third-party style JSON. The entry points are attribution strings that come from untrusted styles, and custom attributions that users can supply
- ✓If you load the library from a CDN, check the version in that URL too. Fixing package.json does nothing if your HTML still pulls an old build
Where that score comes from. The 10.0 above is the CVSS v3.1 score assigned by GitHub, the CNA for this CVE. The vector is `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N`.
NVD does not yet carry a NIST Primary score for it. At the time of writing the NVD record is in the "Received" state and the only metric shown is GitHub's. NIST may publish a different number later. If you open NVD and see something else, that is why.
The sanitizer skips every second dangerous attribute
MapLibre passes attribution HTML through `DOM.sanitize()` before rendering it. Inside, the routine that strips dangerous attributes iterated `elem.attributes` directly while calling `elem.removeAttribute()` in the same loop.
`elem.attributes` is a NamedNodeMap, a live collection that always reflects the element's current state. Remove one attribute and every attribute after it shifts down by one index. The iterator, meanwhile, advances to the next index, so it steps straight over the attribute that just moved into the slot it already passed.
<!-- The example given in the maintainers' advisory -->
<details open onload="1" ontoggle="...">
<!-- onload is removed -> ontoggle shifts down one index
-> the iterator moves to the next index and skips ontoggle
-> ontoggle survives and is inserted into innerHTML -->Two dangerous attributes side by side is all it takes: the second one gets through. Whatever survives runs the moment the attribution control drops the content into `innerHTML`.
The maintainers describe this as zero-click XSS. The only thing the victim has to do is render the map. There is no link to follow and no button to press.
A one-line fix, and a pattern worth checking in your own code
// removeAttributes() in src/util/dom.ts
- for (const {name, value} of elem.attributes) {
+ for (const {name, value} of Array.from(elem.attributes)) {
if (!DOM.isPossiblyDangerous(name, value)) continue;
elem.removeAttribute(name);
}That is the whole fix: take a static snapshot with `Array.from()` before iterating. The patch commit is `1da69f3`, one line of source plus thirteen lines of test.
This is not a MapLibre-specific mistake. Mutating a live collection while iterating it is a standard JavaScript trap. `document.getElementsByClassName()`, `element.children` and `element.attributes` are all live; the NodeList returned by `querySelectorAll()` is static and behaves differently.
If you maintain your own sanitizer or DOM cleanup pass, go look for this shape today. A loop that deletes while it iterates should either snapshot with `Array.from()` or walk backwards.
Check whether you are affected
# See the installed version and where it came from
npm ls maplibre-gl
# yarn / pnpm
yarn why maplibre-gl
pnpm why maplibre-gl- ▸Affected: the `maplibre-gl` npm package, 6.4.0 and earlier (the advisory states the range as `<= 6.4.0`)
- ▸Fixed in: 6.4.1, published August 18, 2026
- ▸Latest release as of September 4, 2026: 6.7.0, published September 2, 2026
- ▸Weakness: CWE-79 (cross-site scripting)
- ▸Maintainer advisory: GHSA-jrc7-96c5-q579, published August 19, 2026 and updated September 3, 2026
The dates are worth lining up. Version 6.4.1 shipped on August 18, 2026 and the advisory went out on August 19. But the flaw only appeared on NVD as CVE-2026-85061 on September 3, 2026.
So the fix has been available for almost three weeks. What decides whether you are done, though, is the version you actually have — nothing else. 6.4.1 or later means you are done. "We updated in August" does not answer the question, because 6.4.0, which is affected, shipped on August 16.
If your process only reacts when a CVE identifier shows up in a feed, this one cost you those three weeks.
One note on the 1.x line, checked directly against the maintainers' source for this article. The advisory expresses the affected range as `<= 6.4.0`, which numerically includes 1.15.3. However, the v1.15.3 source (`src/util/dom.js`) contains no `sanitize` function at all. The vulnerable `DOM.sanitize()` belongs to the 6.x code. That is not a maintainer statement — it is something this article verified by opening the source at that tag.
What to take away
"We sanitize it" is a claim that rests on the sanitizer being correct. Here the code doing the neutralizing dropped one attribute on the floor. Distrusting input matters, and so does having tests on the neutralizing step itself — note that the fix shipped with thirteen lines of test covering exactly this case.
And one more thing: front-end libraries arrive through paths you did not choose. You may only show a map on one page, but an admin panel or an internal tool can be carrying the same package transitively. `npm ls` takes a minute.
