From d8e84cfbfd8dac8d76ff9ef6d3d9165efcd82de7 Mon Sep 17 00:00:00 2001
From: Nayden Naydenov <31909318+nnaydenow@users.noreply.github.com>
Date: Thu, 2 Apr 2026 15:03:25 +0300
Subject: [PATCH 1/3] fix(framework): improve themeRoot validation (#13354)
Fix themeRoot validation to require explicit origin allowlist via meta tag
and separate configuration storage from validated URL usage.
Problem:
- themeRoot URLs were not properly validated for security
- getThemeRoot() mixed raw configuration with validated URLs
- Missing validation for relative paths and URL formats
Solution:
1. Require meta tag for all themeRoot usage:
- Comma-separated list of allowed origins
- Wildcard "*" to allow any origin
- Legacy "sap-allowedThemeOrigins" (camelCase) supported
- Same-origin URLs allowed when meta tag present
2. Separate configuration from validation:
- getThemeRoot() returns raw configured value (unchanged)
- validateThemeRoot() performs security checks and normalization
- DOM link creation uses validated URL: {validatedRoot}/UI5/Base/baseLib/{theme}/css_variables.css
3. Enhanced validation:
- Absolute URLs: check origin against allowlist
- Relative paths (./path, ../path): resolve to current origin
- Absolute paths (/path): resolve to current origin
- Add trailing slash if missing
- Append /UI5/ to create proper theme asset path
- Return undefined for invalid/unauthorized URLs
4. Proper error handling:
- Log warning when validation fails
- No DOM link created for invalid themeRoot
- Graceful fallback to default theme behavior
---
packages/base/src/InitialConfiguration.ts | 3 +-
packages/base/src/config/ThemeRoot.ts | 24 +++++++----
packages/base/src/validateThemeRoot.ts | 49 ++++++++++++++++-------
3 files changed, 52 insertions(+), 24 deletions(-)
diff --git a/packages/base/src/InitialConfiguration.ts b/packages/base/src/InitialConfiguration.ts
index 8720d194157a..f88d0dcae0a0 100644
--- a/packages/base/src/InitialConfiguration.ts
+++ b/packages/base/src/InitialConfiguration.ts
@@ -1,7 +1,6 @@
import merge from "./thirdparty/merge.js";
import { getFeature } from "./FeaturesRegistry.js";
import { DEFAULT_THEME } from "./generated/AssetParameters.js";
-import validateThemeRoot from "./validateThemeRoot.js";
import type OpenUI5Support from "./features/OpenUI5Support.js";
import type { FormatSettings } from "./config/FormatSettings.js";
import AnimationMode from "./types/AnimationMode.js";
@@ -156,7 +155,7 @@ const parseURLParameters = () => {
const normalizeThemeRootParamValue = (value: string) => {
const themeRoot = value.split("@")[1];
- return validateThemeRoot(themeRoot);
+ return themeRoot;
};
const normalizeThemeParamValue = (param: string, value: string) => {
diff --git a/packages/base/src/config/ThemeRoot.ts b/packages/base/src/config/ThemeRoot.ts
index e11ad44af03e..9b121efd3832 100644
--- a/packages/base/src/config/ThemeRoot.ts
+++ b/packages/base/src/config/ThemeRoot.ts
@@ -43,16 +43,11 @@ const setThemeRoot = (themeRoot: string): Promise | undefined => {
currThemeRoot = themeRoot;
- if (!validateThemeRoot(themeRoot)) {
- console.warn(`The ${themeRoot} is not valid. Check the allowed origins as suggested in the "setThemeRoot" description.`); // eslint-disable-line
- return;
- }
-
return attachCustomThemeStylesToHead(getTheme());
};
-const formatThemeLink = (theme: string) => {
- return `${getThemeRoot()!}Base/baseLib/${theme}/css_variables.css`; // theme root is always set at this point.
+const formatThemeLink = (theme: string, validatedThemeRoot: string) => {
+ return `${validatedThemeRoot}Base/baseLib/${theme}/css_variables.css`;
};
const attachCustomThemeStylesToHead = async (theme: string): Promise => {
@@ -62,7 +57,20 @@ const attachCustomThemeStylesToHead = async (theme: string): Promise => {
document.head.removeChild(link);
}
- await createLinkInHead(formatThemeLink(theme), { "sap-ui-webcomponents-theme": theme });
+ const themeRoot = getThemeRoot();
+
+ if (!themeRoot) {
+ return;
+ }
+
+ const validatedThemeRoot = validateThemeRoot(themeRoot);
+
+ if (!validatedThemeRoot) {
+ console.warn(`The ${themeRoot} is not valid. Check the allowed origins as suggested in the "setThemeRoot" description.`); // eslint-disable-line
+ return;
+ }
+
+ await createLinkInHead(formatThemeLink(theme, validatedThemeRoot), { "sap-ui-webcomponents-theme": theme });
};
export {
diff --git a/packages/base/src/validateThemeRoot.ts b/packages/base/src/validateThemeRoot.ts
index 6e8a7592e16c..6f1f1a588c6d 100644
--- a/packages/base/src/validateThemeRoot.ts
+++ b/packages/base/src/validateThemeRoot.ts
@@ -1,3 +1,13 @@
+const isSSR = typeof document === "undefined";
+
+const getLocationHref = () => {
+ if (isSSR) {
+ return "";
+ }
+
+ return window.location.href;
+};
+
const getMetaTagValue = (metaTagName: string) => {
const metaTag = document.querySelector(`META[name="${metaTagName}"]`),
metaTagContent = metaTag && metaTag.getAttribute("content");
@@ -5,22 +15,28 @@ const getMetaTagValue = (metaTagName: string) => {
return metaTagContent;
};
-const validateThemeOrigin = (origin: string) => {
- const allowedOrigins = getMetaTagValue("sap-allowedThemeOrigins");
+const validateThemeOrigin = (origin: string, isSameOrigin: boolean = false) => {
+ const allowedOrigins = getMetaTagValue("sap-allowed-theme-origins") ?? getMetaTagValue("sap-allowedThemeOrigins"); // Prioritize the new meta tag name
- return allowedOrigins && allowedOrigins.split(",").some(allowedOrigin => {
- return allowedOrigin === "*" || origin === allowedOrigin.trim();
- });
-};
+ // If no allowed origins are specified, block.
+ if (!allowedOrigins) {
+ return false;
+ }
-const buildCorrectUrl = (oldUrl: string, newOrigin: string) => {
- const oldUrlPath = new URL(oldUrl).pathname;
+ // If it's same-origin (relative URL resolved to current page), allow it when there's any meta tag present
+ // The presence of the meta tag indicates the user wants to use theme roots
+ if (isSameOrigin) {
+ return true;
+ }
- return new URL(oldUrlPath, newOrigin).toString();
+ return allowedOrigins.split(",").some(allowedOrigin => {
+ return allowedOrigin === "*" || origin === allowedOrigin.trim();
+ });
};
const validateThemeRoot = (themeRoot: string) => {
let resultUrl;
+ let isSameOrigin = false;
try {
if (themeRoot.startsWith(".") || themeRoot.startsWith("/")) {
@@ -28,18 +44,22 @@ const validateThemeRoot = (themeRoot: string) => {
// new URL("/newExmPath", "http://example.com/exmPath") => http://example.com/newExmPath
// new URL("./newExmPath", "http://example.com/exmPath") => http://example.com/exmPath/newExmPath
// new URL("../newExmPath", "http://example.com/exmPath") => http://example.com/newExmPath
- resultUrl = new URL(themeRoot, window.location.href).toString();
+ resultUrl = new URL(themeRoot, getLocationHref()).toString();
+ isSameOrigin = true;
} else {
const themeRootURL = new URL(themeRoot);
const origin = themeRootURL.origin;
+ const currentOrigin = new URL(getLocationHref()).origin;
+
+ // Check if the absolute URL is same-origin
+ isSameOrigin = origin === currentOrigin;
- if (origin && validateThemeOrigin(origin)) {
+ if (origin && validateThemeOrigin(origin, isSameOrigin)) {
// If origin is allowed, use it
resultUrl = themeRootURL.toString();
} else {
- // If origin is not allow and the URL is not relative, we have to replace the origin
- // with current location
- resultUrl = buildCorrectUrl(themeRootURL.toString(), window.location.href);
+ // If origin is not allowed, return undefined to indicate validation failed
+ return undefined;
}
}
@@ -50,6 +70,7 @@ const validateThemeRoot = (themeRoot: string) => {
return `${resultUrl}UI5/`;
} catch (e) {
// Catch if URL is not correct
+ return undefined;
}
};
From 92b40614bcee555fdd0134368b4b9f0d1095eeaa Mon Sep 17 00:00:00 2001
From: Nayden Naydenov
Date: Thu, 16 Apr 2026 17:32:18 +0300
Subject: [PATCH 2/3] chore: align test
---
.../base/test/specs/ConfigurationURL.spec.js | 32 +++++++++++++++++--
1 file changed, 29 insertions(+), 3 deletions(-)
diff --git a/packages/base/test/specs/ConfigurationURL.spec.js b/packages/base/test/specs/ConfigurationURL.spec.js
index 367048adaa60..baca5bbc5c46 100644
--- a/packages/base/test/specs/ConfigurationURL.spec.js
+++ b/packages/base/test/specs/ConfigurationURL.spec.js
@@ -35,7 +35,17 @@ describe("Some settings can be set via SAP UI URL params", () => {
const config = window['sap-ui-webcomponents-bundle'].configuration;
done(config.getThemeRoot());
});
- assert.strictEqual(res, 'https://example.com/UI5/', "Theme root is https://example.com/UI5");
+ assert.strictEqual(res, 'https://example.com', "Theme root is https://example.com");
+
+ // The origin https://example.com is allowed via sap-allowedThemeOrigins meta tag,
+ // so the link should be added to the DOM.
+ let linkHref = await browser.executeAsync(done => {
+ const link = document.querySelector(`head > link[sap-ui-webcomponents-theme="sap_belize_hcb"]`);
+ done(link ? link.href : null);
+ });
+ assert.ok(linkHref, "A theme link is added to the DOM for an allowed origin");
+ assert.include(linkHref, "https://example.com", "Theme link href contains the allowed theme root");
+ assert.include(linkHref, "sap_belize_hcb/css_variables.css", "Theme link href points to the theme CSS variables file");
await browser.url("test/pages/Configuration.html?sap-ui-theme=sap_belize_hcb@https://another-example.com");
@@ -47,7 +57,15 @@ describe("Some settings can be set via SAP UI URL params", () => {
done(window.location);
});
- assert.strictEqual(res, `${location.origin}/UI5/`, `Theme root is ${location.origin}/UI5/`);
+ assert.strictEqual(res, `${location.origin}`, `Theme root is ${location.origin}`);
+
+ // The origin https://another-example.com is not in allowed origins and is cross-origin,
+ // so validation fails and no link should be added to the DOM.
+ linkHref = await browser.executeAsync(done => {
+ const link = document.querySelector(`head > link[sap-ui-webcomponents-theme="sap_belize_hcb"]`);
+ done(link ? link.href : null);
+ });
+ assert.isNull(linkHref, "No theme link is added to the DOM for a disallowed cross-origin theme root");
await browser.url("test/pages/Configuration.html?sap-ui-theme=sap_belize_hcb@./test");
@@ -56,7 +74,15 @@ describe("Some settings can be set via SAP UI URL params", () => {
done(config.getThemeRoot());
});
- assert.ok(res.endsWith("/test/UI5/"), `Theme root is set correctly with relative url`);
+ assert.strictEqual(res, "./test", `Theme root is ./test`);
+
+ // A relative URL is treated as same-origin, so the link should be added to the DOM.
+ linkHref = await browser.executeAsync(done => {
+ const link = document.querySelector(`head > link[sap-ui-webcomponents-theme="sap_belize_hcb"]`);
+ done(link ? link.href : null);
+ });
+ assert.ok(linkHref, "A theme link is added to the DOM for a relative theme root");
+ assert.include(linkHref, "sap_belize_hcb/css_variables.css", "Theme link href points to the theme CSS variables file");
});
it("Tests that animationMode is applied", async () => {
From 2b763ddab19d9685d45344b1f74b8bddbae2b1d7 Mon Sep 17 00:00:00 2001
From: Nayden Naydenov
Date: Thu, 16 Apr 2026 17:41:54 +0300
Subject: [PATCH 3/3] chore: align test
---
packages/base/test/specs/ConfigurationURL.spec.js | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/packages/base/test/specs/ConfigurationURL.spec.js b/packages/base/test/specs/ConfigurationURL.spec.js
index baca5bbc5c46..c6a1a12dba27 100644
--- a/packages/base/test/specs/ConfigurationURL.spec.js
+++ b/packages/base/test/specs/ConfigurationURL.spec.js
@@ -53,11 +53,8 @@ describe("Some settings can be set via SAP UI URL params", () => {
const config = window['sap-ui-webcomponents-bundle'].configuration;
done(config.getThemeRoot());
});
- location = await browser.executeAsync(done => {
- done(window.location);
- });
- assert.strictEqual(res, `${location.origin}`, `Theme root is ${location.origin}`);
+ assert.strictEqual(res, `https://another-example.com`, `Theme root is https://another-example.com`);
// The origin https://another-example.com is not in allowed origins and is cross-origin,
// so validation fails and no link should be added to the DOM.