From d71e94f43ac2f6292f6ac85c5935b08e7ceeeb12 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Mon, 1 Jun 2026 13:26:29 +0300 Subject: [PATCH 1/2] Restrict /updateTheme to the configured theme file and harden market host verification --- server/express.js | 7 +++++++ server/url-helper.js | 25 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/server/express.js b/server/express.js index ad9b74c5754..b412fe21532 100644 --- a/server/express.js +++ b/server/express.js @@ -704,6 +704,13 @@ function makeApp(authAddress, cdapConfig, uiSettings) { if (!uiThemePath) { return res.status(500).send('UnKnown theme file. Please make sure the path is valid'); } + // Only allow (re)loading the theme file configured by the operator. Without + // this check, extractUITheme() would __non_webpack_require__() an arbitrary + // absolute path taken from the request body, letting a caller load any file + // on the server as a Node module. + if (uiThemePath !== cdapConfig['ui.theme.file']) { + return res.status(400).send('Invalid theme file path'); + } try { uiThemeConfig = uiThemeWrapper.extractUITheme(cdapConfig, uiThemePath); } catch (e) { diff --git a/server/url-helper.js b/server/url-helper.js index 3bbc13b0d58..6712dd9d810 100644 --- a/server/url-helper.js +++ b/server/url-helper.js @@ -56,7 +56,30 @@ function extractMarketUrls(cdapConfig) { export const getMarketUrls = memoize(extractMarketUrls); export function isVerifiedMarketHost(cdapConfig, url) { - return !!getMarketUrls(cdapConfig).find((element) => url.startsWith(element)); + // A naive `url.startsWith(element)` lets an attacker bypass the allowlist with + // origins such as `https://market.cdap.io.evil.com/...` or + // `https://market.cdap.io@evil.com/...`, turning the market proxy into an + // SSRF. Parse both URLs and require an exact origin match plus a path that is + // contained within the configured market base path. + let requested; + try { + requested = new URL(url); + } catch (e) { + return false; + } + return !!getMarketUrls(cdapConfig).find((element) => { + let base; + try { + base = new URL(element); + } catch (e) { + return false; + } + if (requested.origin !== base.origin) { + return false; + } + const basePath = base.pathname.endsWith('/') ? base.pathname : `${base.pathname}/`; + return requested.pathname === base.pathname || requested.pathname.startsWith(basePath); + }); } export function constructUrl(cdapConfig, path, origin = REQUEST_ORIGIN_ROUTER) { From 9fa0f6ee138bcc237d700c195ba773a048263b4a Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Mon, 1 Jun 2026 13:33:05 +0300 Subject: [PATCH 2/2] Cache parsed market base URLs to avoid re-parsing per request --- server/url-helper.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/server/url-helper.js b/server/url-helper.js index 6712dd9d810..b16b335d9a9 100644 --- a/server/url-helper.js +++ b/server/url-helper.js @@ -55,6 +55,29 @@ function extractMarketUrls(cdapConfig) { export const getMarketUrls = memoize(extractMarketUrls); +// Cache of parsed market base URLs, keyed by the (memoized) array returned by +// getMarketUrls so the configured URLs are only parsed once per config rather +// than on every request. +const parsedMarketUrlsCache = new WeakMap(); + +function getParsedMarketUrls(cdapConfig) { + const marketUrls = getMarketUrls(cdapConfig); + let parsed = parsedMarketUrlsCache.get(marketUrls); + if (!parsed) { + parsed = marketUrls + .map((element) => { + try { + return new URL(element); + } catch (e) { + return null; + } + }) + .filter((parsedUrl) => parsedUrl !== null); + parsedMarketUrlsCache.set(marketUrls, parsed); + } + return parsed; +} + export function isVerifiedMarketHost(cdapConfig, url) { // A naive `url.startsWith(element)` lets an attacker bypass the allowlist with // origins such as `https://market.cdap.io.evil.com/...` or @@ -67,13 +90,7 @@ export function isVerifiedMarketHost(cdapConfig, url) { } catch (e) { return false; } - return !!getMarketUrls(cdapConfig).find((element) => { - let base; - try { - base = new URL(element); - } catch (e) { - return false; - } + return getParsedMarketUrls(cdapConfig).some((base) => { if (requested.origin !== base.origin) { return false; }