From 67dd43bc8a843c4c5b8a628b60afdf2084530069 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 11 Sep 2026 14:55:37 +0200 Subject: [PATCH] fix(maps): backport attribution sanitizer fix without dropping WebGL1 --- client/package.json | 8 ++-- client/scripts/patch-maplibre.mjs | 34 +++++++++++++++ client/tests/unit/maplibre-sanitizer.test.ts | 45 ++++++++++++++++++++ package-lock.json | 5 ++- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 client/scripts/patch-maplibre.mjs create mode 100644 client/tests/unit/maplibre-sanitizer.test.ts diff --git a/client/package.json b/client/package.json index 37e131919..0cda4972f 100644 --- a/client/package.json +++ b/client/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "prebuild": "node scripts/generate-icons.mjs", + "prebuild": "node scripts/patch-maplibre.mjs && node scripts/generate-icons.mjs", "build": "vite build", "build:analyze": "vite build --mode analyze", "preview": "vite preview", @@ -26,7 +26,9 @@ "shots:promote": "node e2e/screenshots/promote.mjs", "e2e:report": "playwright show-report", "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.css\"", - "format:check": "prettier --check \"src/**/*.tsx\" \"src/**/*.css\"" + "format:check": "prettier --check \"src/**/*.tsx\" \"src/**/*.css\"", + "postinstall": "node scripts/patch-maplibre.mjs", + "predev": "node scripts/patch-maplibre.mjs" }, "dependencies": { "@fontsource/bebas-neue": "^5.3.0", @@ -49,7 +51,7 @@ "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", - "maplibre-gl": "^5.24.0", + "maplibre-gl": "5.24.0", "marked": "^18.0.0", "plyr": "^3.8.4", "react": "^19.2.6", diff --git a/client/scripts/patch-maplibre.mjs b/client/scripts/patch-maplibre.mjs new file mode 100644 index 000000000..d9f99a89d --- /dev/null +++ b/client/scripts/patch-maplibre.mjs @@ -0,0 +1,34 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' + +const require = createRequire(import.meta.url) +const packagePath = require.resolve('maplibre-gl/package.json') +const { version } = JSON.parse(readFileSync(packagePath, 'utf8')) + +if (version !== '5.24.0') throw new Error('Recheck the MapLibre sanitizer backport before changing its version') + +// Backport maplibre-gl-js#8189 without removing WebGL1 support in version 6. +const loops = [ + ['of elem.attributes)', 'of Array.from(elem.attributes))'], + ['of e.attributes)', 'of Array.from(e.attributes))'], +] + +for (const filename of [ + 'src/util/dom.ts', + 'dist/maplibre-gl.js', + 'dist/maplibre-gl-dev.js', + 'dist/maplibre-gl-csp.js', + 'dist/maplibre-gl-csp-dev.js', +]) { + const target = join(dirname(packagePath), filename) + const code = readFileSync(target, 'utf8') + const loop = loops.find(([before, after]) => code.includes(before) || code.includes(after)) + if (!loop) throw new Error(`MapLibre sanitizer loop not found in ${filename}`) + const [before, after] = loop + if (code.split(before).length === 1 && code.split(after).length === 2) continue + if (code.split(before).length !== 2 || code.includes(after)) { + throw new Error(`Unexpected MapLibre sanitizer in ${filename}`) + } + writeFileSync(target, code.replace(before, after)) +} diff --git a/client/tests/unit/maplibre-sanitizer.test.ts b/client/tests/unit/maplibre-sanitizer.test.ts new file mode 100644 index 000000000..2705d05df --- /dev/null +++ b/client/tests/unit/maplibre-sanitizer.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import maplibregl from 'maplibre-gl' + +function attribution(html: string, fromSource = false) { + const control = new maplibregl.AttributionControl({ + compact: false, + customAttribution: fromSource ? undefined : html, + }) + const map = { + style: { + tileManagers: fromSource + ? { tiles: { used: true, getSource: () => ({ attribution: html }) } } + : {}, + }, + _getUIString: () => 'Toggle attribution', + on: () => {}, + off: () => {}, + getCanvasContainer: () => document.createElement('div'), + } as unknown as maplibregl.Map + return control.onAdd(map) +} + +describe('MapLibre attribution sanitizer backport', () => { + it.each([false, true])('removes adjacent event handlers (source: %s)', fromSource => { + const container = attribution('
Credit
', fromSource) + const details = container.querySelector('.maplibregl-ctrl-attrib-inner details')! + expect(details.hasAttribute('onload')).toBe(false) + expect(details.hasAttribute('ontoggle')).toBe(false) + expect(details.textContent).toBe('Credit') + }) + + it('removes consecutive dangerous URLs and handlers in nested markup', () => { + const container = attribution('Credit') + const link = container.querySelector('.maplibregl-ctrl-attrib-inner a')! + expect(link.attributes.length).toBe(0) + }) + + it('preserves provider credit and safe links', () => { + const container = attribution('OpenStreetMap') + const link = container.querySelector('.maplibregl-ctrl-attrib-inner a')! + expect(link.getAttribute('href')).toBe('https://www.openstreetmap.org/copyright') + expect(link.getAttribute('rel')).toBe('noopener') + expect(link.textContent).toBe('OpenStreetMap') + }) +}) diff --git a/package-lock.json b/package-lock.json index c13d0ee17..76976674b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", - "maplibre-gl": "^5.24.0", + "maplibre-gl": "5.24.0", "marked": "^18.0.0", "plyr": "^3.8.4", "react": "^19.2.6", @@ -99,7 +99,8 @@ "vite": "8.1.0", "vite-plugin-pwa": "^1.3.0", "vitest": "^4.1.11" - } + }, + "hasInstallScript": true }, "client/node_modules/cookie": { "version": "1.1.1",