Crosstalk-Solutions/project-nomad · critical · Error
Invalid world basemap path
Error message
Invalid world basemap path
What it means
_setupWorldBasemap resolves the destination path for WORLD_BASEMAP_FILENAME under storage/maps/pmtiles and enforces a path-containment guard: the resolved filepath must start with basePath + sep. If not, the configured filename/path escapes the pmtiles directory and setup aborts.
Source
Thrown at admin/app/services/map_service.ts:457
*
* Memoizes success in-process, and de-duplicates concurrent callers via a
* shared in-flight promise so two simultaneous `/maps` requests on a cold
* start don't both launch `pmtiles extract` against the same output path.
*/
private async ensureWorldBasemap(): Promise<void> {
if (this.worldBasemapReady) return
if (this.worldBasemapInFlight) return this.worldBasemapInFlight
this.worldBasemapInFlight = this._setupWorldBasemap().finally(() => {
this.worldBasemapInFlight = null
})
return this.worldBasemapInFlight
}
private async _setupWorldBasemap(): Promise<void> {
const basePath = resolve(join(this.baseDirPath, 'pmtiles'))
const filepath = resolve(join(basePath, WORLD_BASEMAP_FILENAME))
if (!filepath.startsWith(basePath + sep)) {
throw new Error('Invalid world basemap path')
}
await ensureDirectoryExists(basePath)
const existing = await getFileStatsIfExists(filepath)
if (existing && Number(existing.size) > 0) {
this.worldBasemapReady = true
return
}
const info = await this.getGlobalMapInfo()
const args = buildPmtilesExtractArgs({
sourceUrl: info.url,
outputFilepath: filepath,
maxzoom: WORLD_BASEMAP_MAX_ZOOM,
downloadThreads: 4,
})
View on GitHub (pinned to 0bd1c6f4f9)
Solutions
- Keep WORLD_BASEMAP_FILENAME a bare filename with no slashes or '..' segments
- Audit mapStoragePath/baseDirPath configuration for '..' or absolute overrides
- Restore the constant to its shipped default and re-run ensureWorldBasemap
Example fix
// before const WORLD_BASEMAP_FILENAME = '../shared/world.pmtiles' // after const WORLD_BASEMAP_FILENAME = 'world.pmtiles'
Defensive patterns
Strategy: validation
Validate before calling
const SAFE = /^[\w.-]+$/
if (!SAFE.test(WORLD_BASEMAP_FILENAME)) throw new Error('basemap filename must be a bare filename') Type guard
const isBareFilename = (name: string): name is string => /^[\w.-]+$/.test(name) && !name.includes('..') Try / catch
try { await mapService.ensureWorldBasemap() } catch (e) { if (e instanceof Error && e.message === 'Invalid world basemap path') { /* audit WORLD_BASEMAP_FILENAME / storage path config */ } throw e } Prevention
- Never derive basemap filenames from user input
- Keep config-controlled filenames restricted to [A-Za-z0-9._-]
- Test path-containment guards with traversal payloads in CI
When it happens
Trigger: WORLD_BASEMAP_FILENAME being changed (or injected) to a value containing '../' or an absolute path such as '/etc/cron.d/evil', so resolve() produces a path outside basePath. It can also fire if baseDirPath itself is misconfigured so the join result escapes the expected root.
Common situations: Custom builds that override WORLD_BASEMAP_FILENAME with a path instead of a bare filename; env-driven storage path configs that are relative or contain traversal; supply-chain/config tampering this guard is designed to catch.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27).
Data as JSON: /api/errors/ec9fc054ae600578.
Report an issue: GitHub.