doxa-maps/internal/check-cdn-pins.mjs
Doxa Maps 4a12b009aa Every runtime CDN script and stylesheet is an exact version with an integrity hash
maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime loader set `integrity`,
so a CDN could serve any bytes to every partner's visitors unnoticed (CRITIC-5).

- library/constants/cdn.js is the one registry: mapbox-gl 3.24.0, maplibre-gl 5.24.0 (the release the
  @5 range already resolved to), both geocoders (1.9.4 / 5.0.0) and d3 7.9.0, each with its sha384.
  Every host answers Access-Control-Allow-Origin: *, so integrity + crossorigin work cross-origin.
- ensureEngineScript / ensureEngineCSS, the geocoder loaders (library and PPLR) and the generator's
  static MapLibre tags set integrity + crossorigin from it; the embed snippet's engine preload matches.
- check:cdn-pins (gates:post): no floating CDN url in shipped code, and every one is in the registry;
  --online re-fetches each and compares the hash (all 9 match today).

MapLibre stays on 5.x knowingly. GHSA-jrc7-96c5-q579 (DOM.sanitize, reached through a source's
attribution string) affects <= 6.4.0, and 6.x is ESM-only (no UMD global), so the upgrade is a loader
migration of its own. The attribution strings rendered today come from our configured basemap styles
and our own sources; the note in cdn.js says who is exposed until then.
2026-09-26 05:50:24 +00:00

57 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
/**
* check-cdn-pins — every runtime third-party script/stylesheet is an EXACT version with an integrity hash.
*
* CRITIC-5 (2026-09-26): maplibre-gl@5 and maplibre-gl-geocoder@1 were floating ranges, and no runtime
* loader set `integrity`, so a CDN could serve any bytes to every partner's visitors unnoticed.
* library/constants/cdn.js is now the one registry; loaders set integrity + crossorigin from it.
*
* OFFLINE (default, in gates:post): in shipped code (library/, plugin-designs/ except scripts/,
* internal/generate-tree.mjs) no jsDelivr/unpkg/esm.sh/api.mapbox.com JS or CSS URL is a floating
* range (@7, @1.2, latest), and every such URL is either in the registry or on the reviewed list below.
* --online: also re-fetches every registry URL and requires its sha384 to equal the pinned hash.
* EXIT 0 pass · 1 fail · 75 (online) network unavailable
*/
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { CDN } from '../library/constants/cdn.js'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const ONLINE = process.argv.includes('--online')
// Loaded through something other than a <script>/<link> tag, so integrity cannot apply; pinned exactly.
const REVIEWED = new Map([
['https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.3.0/mapbox-gl-rtl-text.js', 'fetched by the engine (setRTLTextPlugin)'],
['https://cdn.jsdelivr.net/npm/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js', 'fetched by the engine (setRTLTextPlugin)'],
])
const known = new Set(Object.values(CDN).map((e) => e.url))
const URL_RE = /https:\/\/(?:cdn\.jsdelivr\.net|unpkg\.com|esm\.sh|api\.mapbox\.com\/mapbox-gl-js)\/[^\s'"`)<>]+?\.(?:m?js|css)\b/g
const FLOATING = /@(?:\d+|\d+\.\d+|latest|next)\/|@\^|@~/
const files = execFileSync('git', ['ls-files', '--', 'library', 'plugin-designs', 'internal/generate-tree.mjs'], { cwd: ROOT, encoding: 'utf8' })
.split('\n').filter((f) => /\.(m?js|vue|html)$/.test(f) && !/\/scripts\//.test(f) && !/\/index\.html$/.test(f) && !/^plugin-designs\/[^/]+\/data\//.test(f))
let bad = 0
const seen = new Map()
for (const f of files) {
const src = execFileSync('git', ['show', `HEAD:${f}`], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 << 20 })
for (const m of src.matchAll(URL_RE)) {
const u = m[0]
if (!seen.has(u)) seen.set(u, f)
if (FLOATING.test(u)) { bad++; console.log(` FAIL ${f}: floating version ${u}`) }
else if (!known.has(u) && !REVIEWED.has(u)) { bad++; console.log(` FAIL ${f}: not in library/constants/cdn.js (no integrity): ${u}`) }
}
}
console.log(`CDN-PINS: ${seen.size} distinct runtime CDN url(s) in shipped code; registry holds ${known.size}.`)
if (ONLINE) {
for (const [k, e] of Object.entries(CDN)) {
let got = ''
try { const r = await fetch(e.url); if (!r.ok) throw new Error('HTTP ' + r.status); got = 'sha384-' + createHash('sha384').update(Buffer.from(await r.arrayBuffer())).digest('base64') }
catch (err) { console.log(`CDN-PINS: NOT MEASURED — ${e.url}: ${err.message}`); process.exit(75) }
const ok = got === e.integrity
if (!ok) bad++
console.log(` ${ok ? 'ok ' : 'FAIL'} ${k}: ${ok ? 'bytes match the pinned hash' : `served ${got}, pinned ${e.integrity}`}`)
}
}
if (bad) { console.log(`CDN-PINS: FAIL — ${bad} problem(s).`); process.exit(1) }
console.log(`CDN-PINS: PASS — every runtime CDN script/stylesheet is an exact version${ONLINE ? ', and every pinned hash matches what the CDN serves' : ' with an integrity hash'}.`)