import { applyGlobalMiddleware, routingCspOrigins } from '../../../src/middleware/globalMiddleware'; import express from 'express'; import request from 'supertest'; import { describe, it, expect, afterEach } from 'vitest'; async function directiveSources(name: string): Promise { const app = express(); applyGlobalMiddleware(app); app.get('/probe', (_req, res) => res.json({ ok: true })); const res = await request(app).get('/probe'); const csp = String(res.headers['content-security-policy'] || ''); const directive = csp .split(';') .map((d) => d.trim()) .find((d) => d.startsWith(name)); return directive ? directive.split(/\s+/).slice(1) : []; } const connectSrcSources = () => directiveSources('connect-src'); describe('global CSP: OpenStreetMap tile hosts (#1733)', () => { it('allows the bare tile.openstreetmap.org host', async () => { // A CSP wildcard host never matches the apex, so `*.tile.openstreetmap.org` // alone would block the tile prefetcher's fetch() against the single host // OSM has served from since it retired a/b/c/d sharding. expect(await connectSrcSources()).toContain('https://tile.openstreetmap.org'); }); it('still allows the sharded hosts for templates saved earlier', async () => { expect(await connectSrcSources()).toContain('https://*.tile.openstreetmap.org'); }); }); describe('global CSP: the other shipped raster presets (#2180)', () => { it('allows tile.openstreetmap.de', async () => { // The prefetcher fetches tiles with mode 'no-cors', which relaxes CORS and // nothing else: a host missing here is refused in the document, so the // Service Worker never sees the request and caches no tile at all. expect(await connectSrcSources()).toContain('https://tile.openstreetmap.de'); }); it('allows tiles.stadiamaps.com', async () => { expect(await connectSrcSources()).toContain('https://tiles.stadiamaps.com'); }); it('allows the OpenTopoMap apex and tile shards used by the Tours planner', async () => { const sources = await connectSrcSources(); expect(sources).toContain('https://tile.opentopomap.org'); expect(sources).toContain('https://*.tile.opentopomap.org'); }); it('allows the OpenSeaMap seamark host a paddle tour can lay over its map', async () => { expect(await connectSrcSources()).toContain('https://tiles.openseamap.org'); }); it('keeps the routing host, which is a different host and covers nothing here', async () => { // routing.openstreetmap.de was on the list all along and looks close enough // to hide the gap: a CSP source matches a host, not a suffix of one. expect(await connectSrcSources()).toContain('https://routing.openstreetmap.de/'); }); }); describe('global CSP: the second routing engine', () => { it('allows the public Valhalla, which is a shipped default like the OSRM hosts', async () => { // It is asked only when a leg should avoid tolls, a motorway or a ferry — the // question the OSRM hosts answer with HTTP 400, because their car profile carries // no excludable classes. Left out here it fails the way CSP always fails a fetch: // silently, with the switch working and no tile of an answer arriving. expect(await connectSrcSources()).toContain('https://valhalla1.openstreetmap.de'); }); it('names the origin without a path, because it is a POST to /route', async () => { // The OSRM entries carry `/route/v1/` because a source with a path matches that // path alone, and OSRM is only ever asked there. Valhalla is a different shape and // would grow endpoints (isochrones, map matching), so the origin is the right unit. const sources = await connectSrcSources(); expect(sources).not.toContain('https://valhalla1.openstreetmap.de/route'); }); }); describe('global CSP: the imagery host (#2307)', () => { it('allows server.arcgisonline.com, which a GL map reaches through fetch', async () => { // The gap hid behind Leaflet for as long as Leaflet was the only renderer to // show the imagery: it loads a tile as an , and img-src allows `https:` // outright. A GL map reads the raster through fetch to hand it to WebGL, so // it lands on connect-src instead and was refused with nothing to see for it. expect(await connectSrcSources()).toContain('https://server.arcgisonline.com'); }); it('names the apex host, because img-src is what used to carry it', async () => { // img-src keeps its blanket `https:`, which is why Leaflet never noticed. The // fix belongs on connect-src alone; widening img-src further would buy nothing // and widening connect-src to a wildcard would not match the apex anyway. const sources = await connectSrcSources(); expect(sources).not.toContain('https://*.arcgisonline.com'); expect(await directiveSources('img-src')).toContain('https:'); }); }); describe('global CSP: the elevation tiles of the 3D tour view', () => { it('allows the Terrarium tiles, which MapLibre and the flyover preload fetch', async () => { // The 3D view drapes the route over Mapzen's open elevation model on AWS Open // Data. MapLibre reads raster-dem tiles through fetch, and the corridor preload // warms the cache with fetch too, so both land on connect-src; img-src is not asked. expect(await connectSrcSources()).toContain('https://elevation-tiles-prod.s3.amazonaws.com'); }); it('names that one bucket host, not every S3 bucket', async () => { const sources = await connectSrcSources(); expect(sources).not.toContain('https://*.s3.amazonaws.com'); expect(sources).not.toContain('https://*.amazonaws.com'); }); }); describe('global CSP: script-src', () => { it("allows 'wasm-unsafe-eval' so the WASM decoders keep running", async () => { expect(await directiveSources('script-src')).toContain("'wasm-unsafe-eval'"); }); it("still allows 'unsafe-eval', which heic-to needs to decode an iPhone photo", async () => { // Pinned so the next tidy-up of this list finds the reason before the // consequence: libheif initialises embind with new Function(), and without // this every .heic upload fails in the browser. See the comment on the // directive for how to actually get rid of it. expect(await directiveSources('script-src')).toContain("'unsafe-eval'"); }); }); describe('global CSP: media-src (#2341)', () => { it('allows blob:, which is where a picked clip lives until it is uploaded', async () => { // The editor previews the clip and grabs its poster frame through a