地図ライブラリ MapLibre GL JS に、CVSS 10.0(CRITICAL) のクロスサイトスクリプティング CVE-2026-85061 があります。npm パッケージ `maplibre-gl` の 6.4.0 以前が対象で、6.4.1 で修正されています。
危険なのは、これがサニタイズ処理そのもののバグだという点です。無害化しているつもりの関数が危険な属性を1つ取りこぼし、地図が帰属表示(attribution)を描画した時点でスクリプトが動きます。クリックなどの追加操作は要りません。
今すぐ行う対策
- ✓npm の `maplibre-gl` を 6.4.1 以降へ更新する。 2026年9月4日時点の最新版は 6.7.0(2026年9月2日公開)
- ✓`npm ls maplibre-gl` で、実際に入っている版と、どの依存から入っているかを確認する。 地図を使う社内ツールや管理画面が間接依存で抱えていることがある
- ✓すぐに更新できない場合は、開発元が示している回避策を使う。 source の `attribution` フィールドを、MapLibre へ渡す前にサニタイズする
- ✓第三者が配布しているスタイル(style JSON)を読み込んでいないか確認する。 攻撃の入口は、信頼できないスタイル由来の帰属表示文字列か、ユーザーが入力できるカスタム帰属表示
- ✓CDN から直接読み込んでいる場合は、そのURLのバージョン番号も確認する。 package.json だけ直しても、HTML側が古い版を読んでいれば直らない
このスコアの出所を明示します。 上の 10.0 は、GitHub(このCVEの採番機関)が付けた CVSS v3.1 の値です。ベクタは `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N`。
NVD にはまだ NIST 自身(Primary)の評価が載っていません。この記事を書いた時点で NVD 上の状態は「Received」で、掲載されている指標は GitHub の値だけです。後日 NIST が別の数字を付ける可能性があります。NVD を開いて違う数字が出ても、版と評価者が違うためだと考えてください。
サニタイズが危険な属性を「1つ飛ばし」で通す
MapLibre は、帰属表示に入る HTML を `DOM.sanitize()` で無害化してから描画します。その内部で、危険な属性を取り除く処理が次のように書かれていました。
`elem.attributes` をそのまま反復しながら、同じループの中で `elem.removeAttribute()` を呼ぶ、という形です。
`elem.attributes` は NamedNodeMap という「生きたコレクション」で、要素の実際の状態をそのまま映します。属性を1つ削ると、後ろの属性のインデックスが1つ前へ詰まります。ところが反復子は次のインデックスへ進むので、詰まってきた属性を1つ読み飛ばします。
<!-- 開発元のアドバイザリが挙げている例 -->
<details open onload="1" ontoggle="...">
<!-- onload が削除される → ontoggle のインデックスが1つ前へ詰まる
→ 反復子は次のインデックスへ進むので ontoggle を読み飛ばす
→ ontoggle が生き残ったまま innerHTML へ挿入される -->つまり、危険な属性を2つ隣り合わせに書くだけで、2つ目が素通りします。生き残った属性は、帰属表示のコントロールが内容を `innerHTML` に入れた時点で実行されます。
開発元はこれを ゼロクリックXSS と表現しています。被害者側に必要なのは「その地図を表示すること」だけで、リンクを踏む・ボタンを押すといった追加の操作は要りません。
修正は1行、しかし他人事ではない
// src/util/dom.ts の removeAttributes()
- 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);
}`Array.from()` で静的なスナップショットを取ってから反復するようにした、それだけです。修正コミットは `1da69f3`、変更は本体1行とテスト13行でした。
この形は MapLibre 固有の間違いではありません。「生きたコレクションを反復しながら、その要素を削除する」は JavaScript でよくある落とし穴です。`document.getElementsByClassName()` や `element.children`、`element.attributes` はいずれも生きたコレクションで、`querySelectorAll()` が返す静的な NodeList とは挙動が違います。
自作のサニタイザや DOM のクリーンアップ処理に同じ形が無いか、この機会に見てください。削除しながら回すループは、`Array.from()` か逆順ループにするのが定石です。
自分が影響を受けるか確認する
# 実際に入っている版と、どの依存から入っているかを見る
npm ls maplibre-gl
# yarn / pnpm の場合
yarn why maplibre-gl
pnpm why maplibre-gl- ▸影響を受ける版:npm パッケージ `maplibre-gl` の 6.4.0 以前(開発元アドバイザリの表記は `<= 6.4.0`)
- ▸修正版:6.4.1(2026年8月18日公開)
- ▸2026年9月4日時点の最新版:6.7.0(2026年9月2日公開)
- ▸弱点の種類:CWE-79(クロスサイトスクリプティング)
- ▸開発元アドバイザリ:GHSA-jrc7-96c5-q579(2026年8月19日公開、2026年9月3日更新)
日付の関係を整理しておきます。修正版 6.4.1 が出たのは 2026年8月18日、開発元のアドバイザリは 8月19日の公開です。一方、CVE-2026-85061 として NVD に載ったのは 2026年9月3日でした。
つまり修正自体は3週間近く前から出ています。ただし、対応が終わっているかどうかを決めるのは入っている版だけです。6.4.1 以降なら終わっています。「8月に更新した」は答えになりません。対象である 6.4.0 自体が 8月16日公開だからです。
逆に、CVEの通知だけを見て動く運用をしていると、この3週間ぶん気付くのが遅れたことになります。
なお、1.x 系については本記事で開発元のソースを直接確認しました。アドバイザリの影響範囲表記は `<= 6.4.0` で、数字の上では 1.15.3 も範囲に入ります。しかし v1.15.3 のソース(`src/util/dom.js`)には `sanitize` という関数自体が存在しません。今回の欠陥がある `DOM.sanitize()` は 6.x 系のコードです。これは公式の見解ではなく、本記事が該当タグのソースを開いて確認した事実として書いています。
この件から持ち帰れること
「サニタイズしているから安全」は、サニタイザ自身が正しいことを前提にした話です。今回は無害化する側が1つ取りこぼしました。入力を信用しない設計と同じくらい、無害化処理そのものにテストがあるかが効きます。実際、修正コミットにはこのケースを踏むテストが13行追加されています。
もうひとつ。フロントエンドのライブラリは「自分が入れた覚えがない」経路で入ってきます。地図を出すのは自社サイトの一部だけでも、管理画面や社内ツールが同じパッケージを間接依存で抱えていることがあります。`npm ls` を打つのは1分です。
