From 2b1b9c3c93f5db09e4b4213672e0cc98dee3e60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20K=C3=BCnzi?= Date: Fri, 17 Apr 2026 20:28:30 +0200 Subject: [PATCH 1/2] PB-2553: stopping XSS attacks by whitelisting - Issue: The api_url, wms_url and wmts_url params can currently receive any url as values, which means a malicious party could use them as an attack vector. This is especially true for api_url, which can send html back and is needed to operate that way. - Fix: We have made regexes which enfore a whitelist on those parameters As we need external sources html to be rendered (for the feature informations, for example), we sadly can't sanitize the html given to us without breaking a lot of currently working layers. We can only limit the sources and hope they don't provide malicious content. --- .../storeSync/BaseUrlOverrideParamConfig.class.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js b/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js index c937b11640..4365e9d3e9 100644 --- a/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js +++ b/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js @@ -6,9 +6,22 @@ import { import AbstractParamConfig from '@/router/storeSync/abstractParamConfig.class' import { isValidUrl } from '@/utils/utils' +// //https://sys-{wms,wmts,api3}.{dev,int}.bgdi.ch/ +// or http://localhost{:port_number} +const allowed_url_regexes = { + wms_url: /^https:\/\/sys-wms.(dev|int).bgdi.ch\/?$/g, + wmts_url: /^https:\/\/sys-wmts.(dev|int).bgdi.ch\/?$/g, + api3: /^https:\/\/sys-api3.(dev|int).bgdi.ch\/?$/g, + localhost: /^http:\/\/localhost(:\d{2,6})?\/?$/g, +} + export default function createBaseUrlOverrideParamConfig({ urlParamName, baseUrlPropertyName }) { function dispatchBaseUrlOverride(to, store, urlParamValue) { - if (isValidUrl(urlParamValue)) { + if ( + isValidUrl(urlParamValue) && + (allowed_url_regexes[urlParamName]?.match(urlParamValue) || + allowed_url_regexes.localhost.match(urlParamValue)) + ) { setBaseUrlOverrides(baseUrlPropertyName, urlParamValue) } else { setBaseUrlOverrides(baseUrlPropertyName, null) From abdeab875c6cf73c3607ce788b59538381d59425 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Tue, 21 Apr 2026 09:09:35 +0200 Subject: [PATCH 2/2] PB-2553: merge REGEX declaration and add simple unit test --- .../BaseUrlOverrideParamConfig.class.js | 23 +++++----- .../BaseUrlOverrideParamConfig.spec.js | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 packages/mapviewer/src/router/storeSync/__tests__/BaseUrlOverrideParamConfig.spec.js diff --git a/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js b/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js index 4365e9d3e9..27c8a32350 100644 --- a/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js +++ b/packages/mapviewer/src/router/storeSync/BaseUrlOverrideParamConfig.class.js @@ -6,22 +6,21 @@ import { import AbstractParamConfig from '@/router/storeSync/abstractParamConfig.class' import { isValidUrl } from '@/utils/utils' -// //https://sys-{wms,wmts,api3}.{dev,int}.bgdi.ch/ -// or http://localhost{:port_number} -const allowed_url_regexes = { - wms_url: /^https:\/\/sys-wms.(dev|int).bgdi.ch\/?$/g, - wmts_url: /^https:\/\/sys-wmts.(dev|int).bgdi.ch\/?$/g, - api3: /^https:\/\/sys-api3.(dev|int).bgdi.ch\/?$/g, - localhost: /^http:\/\/localhost(:\d{2,6})?\/?$/g, +// https://sys-{wms,wmts,api3}.{dev,int}.bgdi.ch/ or http://localhost{:port_number} +const ALLOWED_BGDI_URL = /^https:\/\/sys-(wms|wmts|api3)\.(dev|int)\.bgdi\.ch(\/[^?]*)?(\?.*)?$/ +const ALLOWED_LOCALHOST_URL = /^http:\/\/localhost(:\d{2,6})?(\/[^?]*)?(\?.*)?$/ + +/** + * To protect against XSS attacks, we only allow the override of the base URL if the URL is + * whitelisted. + */ +export function isBaseUrlOverrideAllowed(url) { + return ALLOWED_BGDI_URL.test(url) || ALLOWED_LOCALHOST_URL.test(url) } export default function createBaseUrlOverrideParamConfig({ urlParamName, baseUrlPropertyName }) { function dispatchBaseUrlOverride(to, store, urlParamValue) { - if ( - isValidUrl(urlParamValue) && - (allowed_url_regexes[urlParamName]?.match(urlParamValue) || - allowed_url_regexes.localhost.match(urlParamValue)) - ) { + if (isValidUrl(urlParamValue) && isBaseUrlOverrideAllowed(urlParamValue)) { setBaseUrlOverrides(baseUrlPropertyName, urlParamValue) } else { setBaseUrlOverrides(baseUrlPropertyName, null) diff --git a/packages/mapviewer/src/router/storeSync/__tests__/BaseUrlOverrideParamConfig.spec.js b/packages/mapviewer/src/router/storeSync/__tests__/BaseUrlOverrideParamConfig.spec.js new file mode 100644 index 0000000000..bbf28ea26f --- /dev/null +++ b/packages/mapviewer/src/router/storeSync/__tests__/BaseUrlOverrideParamConfig.spec.js @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { isBaseUrlOverrideAllowed } from '../BaseUrlOverrideParamConfig.class' + +const services = ['wms', 'wmts', 'api3'] + +describe('isBaseUrlOverrideAllowed', () => { + services.forEach((service) => { + const devUrl = `https://sys-${service}.dev.bgdi.ch` + const intUrl = `https://sys-${service}.int.bgdi.ch` + + it(`allows a PR preview link as override for ${service}`, () => { + expect( + isBaseUrlOverrideAllowed(`${devUrl}/some-pr-path`), + 'DEV PR preview link was not allowed' + ).toBe(true) + expect( + isBaseUrlOverrideAllowed(`${intUrl}/some-pr-path`), + 'INT PR preview link was not allowed' + ).toBe(true) + }) + + it(`allows the use of the DEV or INT staging as override for ${service}`, () => { + expect(isBaseUrlOverrideAllowed(devUrl)).toBe(true) + expect(isBaseUrlOverrideAllowed(intUrl)).toBe(true) + }) + }) + + it('allows the use of localhost URLs as override', () => { + expect(isBaseUrlOverrideAllowed('http://localhost')).toBe(true) + expect(isBaseUrlOverrideAllowed('http://localhost:1234')).toBe(true) + expect(isBaseUrlOverrideAllowed('http://localhost:5678/some-path')).toBe(true) + }) + + it('should return false for a URL not matching either allowed pattern', () => { + expect(isBaseUrlOverrideAllowed('https://unallowed.example.com')).toBe(false) + expect(isBaseUrlOverrideAllowed('https://sys-wrongservice.dev.bgdi.ch')).toBe(false) + }) + + it('should return false for an invalid URL', () => { + expect(isBaseUrlOverrideAllowed('not-a-valid-url')).toBe(false) + }) +})