mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-16 21:00:03 +02:00
fix(maps): gate CORS on method, narrow /api/maps/ to /styles
/api/maps/ has ten POST/PATCH/DELETE routes under it (download-remote, extract, marker CRUD, and others). The middleware set Access-Control-Allow-Origin before checking the method, so those responses got the header too. Gate emission to GET/HEAD/OPTIONS. /styles is the only /api/maps/ route another app needs cross-origin (confirmed against the app that surfaced this bug); match it exactly instead of the whole prefix. The other three prefixes are served by AdonisJS's static provider, GET/HEAD only, so they're unaffected.
This commit is contained in:
parent
2703057f37
commit
442142d1ee
|
|
@ -4,17 +4,24 @@ import type { NextFn } from '@adonisjs/core/types/http'
|
|||
// origin: ['*'] in config/cors.ts never matches a real Origin header
|
||||
// (@adonisjs/cors only wildcards on the bare string origin: '*'), and a
|
||||
// global fix would also expose the write API since credentials: true is
|
||||
// set. Scope anonymous, read-only CORS to the map asset paths instead.
|
||||
// set. Scope anonymous, read-only CORS to map tiles/assets and the one
|
||||
// JSON route another app actually reads, GET/HEAD/OPTIONS only.
|
||||
|
||||
const MAP_ASSET_PREFIXES = ['/api/maps/', '/pmtiles/', '/basemaps-assets/', '/storage/maps/']
|
||||
const MAP_ASSET_EXACT_PATHS = ['/api/maps/styles']
|
||||
const MAP_ASSET_PREFIXES = ['/pmtiles/', '/basemaps-assets/', '/storage/maps/']
|
||||
const READABLE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])
|
||||
|
||||
export function isMapAssetPath(pathname: string): boolean {
|
||||
return MAP_ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix))
|
||||
return MAP_ASSET_EXACT_PATHS.includes(pathname) || MAP_ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix))
|
||||
}
|
||||
|
||||
export function isReadableMethod(method: string): boolean {
|
||||
return READABLE_METHODS.has(method)
|
||||
}
|
||||
|
||||
export default class MapsCorsMiddleware {
|
||||
async handle({ request, response }: HttpContext, next: NextFn) {
|
||||
if (!isMapAssetPath(request.url())) return next()
|
||||
if (!isMapAssetPath(request.url()) || !isReadableMethod(request.method())) return next()
|
||||
|
||||
// safeHeader so this stays inert if the global CORS config is ever fixed.
|
||||
response.safeHeader('Access-Control-Allow-Origin', '*')
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import * as assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { isMapAssetPath } from '../../app/middleware/maps_cors_middleware.js'
|
||||
import { isMapAssetPath, isReadableMethod } from '../../app/middleware/maps_cors_middleware.js'
|
||||
|
||||
test('isMapAssetPath accepts map style, pmtiles, and basemaps-assets paths', () => {
|
||||
test('isMapAssetPath accepts the styles route, pmtiles, and basemaps-assets paths', () => {
|
||||
for (const path of [
|
||||
'/api/maps/styles',
|
||||
'/api/maps/regions',
|
||||
'/pmtiles/washington.pmtiles',
|
||||
'/basemaps-assets/fonts/Noto%20Sans%20Regular/0-255.pbf',
|
||||
'/basemaps-assets/sprites/v4/light.json',
|
||||
|
|
@ -16,8 +15,27 @@ test('isMapAssetPath accepts map style, pmtiles, and basemaps-assets paths', ()
|
|||
}
|
||||
})
|
||||
|
||||
test('isMapAssetPath rejects non-map paths, including a near-miss without the trailing slash', () => {
|
||||
for (const path of ['/api/system/info', '/api/maps', '/api/mapsfoo', '/maps', '/', '/pmtiles', '/basemaps-assets']) {
|
||||
test('isMapAssetPath rejects non-map paths and other /api/maps/ routes', () => {
|
||||
for (const path of [
|
||||
'/api/system/info',
|
||||
'/api/maps',
|
||||
'/api/mapsfoo',
|
||||
'/api/maps/regions',
|
||||
'/api/maps/download-remote',
|
||||
'/maps',
|
||||
'/',
|
||||
'/pmtiles',
|
||||
'/basemaps-assets',
|
||||
]) {
|
||||
assert.equal(isMapAssetPath(path), false, `${path} should not be a map asset path`)
|
||||
}
|
||||
})
|
||||
|
||||
test('isReadableMethod accepts GET, HEAD, and OPTIONS, rejects write methods', () => {
|
||||
for (const method of ['GET', 'HEAD', 'OPTIONS']) {
|
||||
assert.equal(isReadableMethod(method), true, `${method} should be readable`)
|
||||
}
|
||||
for (const method of ['POST', 'PUT', 'PATCH', 'DELETE']) {
|
||||
assert.equal(isReadableMethod(method), false, `${method} should not be readable`)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user