diff --git a/docs/config.rst b/docs/config.rst index a8c644a..2164080 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -32,6 +32,7 @@ Example: "serveAllFonts": false, "serveAllStyles": false, "serveStaticMaps": true, + "allowRemoteMarkerIcons": true, "tileMargin": 0 }, "styles": { @@ -142,6 +143,13 @@ Optional string to be rendered into the raster tiles (and static maps) as waterm Can be used for hard-coding attributions etc. (can also be specified per-style). Not used by default. +``allowRemoteMarkerIcons`` +-------------- + +Allows the rendering of marker icons fetched via http(s) hyperlinks. +For security reasons only allow this if you can control the origins from where the markers are fetched! +Default is to disallow fetching of icons from remote sources. + ``styles`` ========== diff --git a/package.json b/package.json index 65c960f..2a977d3 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "proj4": "2.8.0", "request": "2.88.2", "sharp": "0.31.0", - "tileserver-gl-styles": "2.0.0" + "tileserver-gl-styles": "2.0.0", + "sanitize-filename": "1.6.3" }, "devDependencies": { "chai": "4.3.6", diff --git a/src/serve_rendered.js b/src/serve_rendered.js index 2909c10..6ad9442 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -8,10 +8,10 @@ import util from 'util'; import zlib from 'zlib'; import sharp from 'sharp'; // sharp has to be required before node-canvas. see https://github.com/lovell/sharp/issues/371 import pkg from 'canvas'; -import Image from 'canvas'; import clone from 'clone'; import Color from 'color'; import express from 'express'; +import sanitize from "sanitize-filename"; import SphericalMercator from '@mapbox/sphericalmercator'; import mlgl from '@maplibre/maplibre-gl-native'; import MBTiles from '@mapbox/mbtiles'; @@ -22,7 +22,7 @@ import {getFontsPbf, getTileUrls, fixTileJSONCenter} from './utils.js'; const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)'; const httpTester = /^(http(s)?:)?\/\//; -const {createCanvas} = pkg; +const {createCanvas, Image} = pkg; const mercator = new SphericalMercator(); const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0; @@ -231,11 +231,20 @@ const extractMarkersFromQuery = (query, options, transformer) => { // Check if icon is served via http otherwise marker icons are expected to // be provided as filepaths relative to configured icon path if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) { - iconURI = path.resolve(options.paths.icons, iconURI); - // Ensure icon exists at provided path - if (!fs.existsSync(iconURI)) { + // Sanitize URI with sanitize-filename + // https://www.npmjs.com/package/sanitize-filename#details + iconURI = sanitize(iconURI) + + // If the selected icon is not part of available icons skip it + if (!options.paths.availableIcons.includes(iconURI)) { continue; } + + iconURI = path.resolve(options.paths.icons, iconURI); + + // When we encounter a remote icon check if the configuration explicitly allows them. + } else if (options.allowRemoteMarkerIcons !== true) { + continue; } // Ensure marker location could be parsed diff --git a/src/server.js b/src/server.js index 9f4f103..b472e9f 100644 --- a/src/server.js +++ b/src/server.js @@ -95,6 +95,35 @@ export function server(opts) { checkPath('mbtiles'); checkPath('icons'); + /** + * Recursively get all files within a directory. + * Inspired by https://stackoverflow.com/a/45130990/10133863 + * @param {String} directory Absolute path to a directory to get files from. + */ + const getFiles = async (directory) => { + // Fetch all entries of the directory and attach type information + const dirEntries = await fs.promises.readdir(directory, { withFileTypes: true }); + + // Iterate through entries and return the relative file-path to the icon directory if it is not a directory + // otherwise initiate a recursive call + const files = await Promise.all(dirEntries.map((dirEntry) => { + const entryPath = path.resolve(directory, dirEntry.name); + return dirEntry.isDirectory() ? + getFiles(entryPath) : entryPath.replace(paths.icons + path.sep, ""); + })); + + // Flatten the list of files to a single array + return files.flat(); + } + + // Load all available icons into a settings object + startupPromises.push(new Promise(resolve => { + getFiles(paths.icons).then((files) => { + paths.availableIcons = files; + resolve(); + }); + })); + if (options.dataDecorator) { try { options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator));