Skip to content

Commit ba4a08b

Browse files
authored
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: <meta name="sap-allowed-theme-origins" content="https://cdn.example.com"> - 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
1 parent 3afe33f commit ba4a08b

7 files changed

Lines changed: 1120 additions & 87 deletions

File tree

packages/base/cypress/specs/ConfigurationChange.cy.tsx

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,202 @@ describe("Some configuration options can be changed at runtime", () => {
3939
.should("deep.equal", newThemeRoot);
4040
});
4141
});
42+
43+
describe("ThemeRoot validation at runtime", () => {
44+
describe("Valid themeRoot with allowed origin", () => {
45+
before(() => {
46+
cy.window()
47+
.then($el => {
48+
const metaTag = document.createElement("meta");
49+
metaTag.name = "sap-allowed-theme-origins";
50+
metaTag.content = "https://runtime-example.com";
51+
$el.document.head.append(metaTag);
52+
});
53+
});
54+
55+
after(() => {
56+
cy.window()
57+
.then($el => {
58+
const metaTag = $el.document.head.querySelector("[name='sap-allowed-theme-origins']");
59+
metaTag?.remove();
60+
const link = $el.document.head.querySelector("link[sap-ui-webcomponents-theme]");
61+
link?.remove();
62+
});
63+
});
64+
65+
it("should set raw themeRoot", () => {
66+
cy.wrap({ setThemeRoot })
67+
.invoke("setThemeRoot", "https://runtime-example.com/themes");
68+
69+
cy.wrap({ getThemeRoot })
70+
.invoke("getThemeRoot")
71+
.should("equal", "https://runtime-example.com/themes");
72+
73+
// Verify link is created in DOM
74+
cy.wrap({ getTheme })
75+
.invoke("getTheme")
76+
.then(theme => {
77+
cy.get(`link[sap-ui-webcomponents-theme="${theme}"]`)
78+
.should("exist")
79+
.and("have.attr", "href")
80+
.then(href => {
81+
return href.includes("https://runtime-example.com/themes/UI5/Base/baseLib/");
82+
})
83+
.should("be.true");
84+
});
85+
});
86+
});
87+
88+
describe("Invalid themeRoot without meta tag", () => {
89+
after(() => {
90+
cy.window()
91+
.then($el => {
92+
const link = $el.document.head.querySelector("link[sap-ui-webcomponents-theme]");
93+
link?.remove();
94+
});
95+
});
96+
97+
it("should set themeRoot but log warning", () => {
98+
const consoleWarnStub = cy.stub().as("consoleWarn");
99+
100+
cy.window().then(win => {
101+
cy.stub(win.console, "warn").callsFake(consoleWarnStub);
102+
});
103+
104+
cy.wrap({ setThemeRoot })
105+
.invoke("setThemeRoot", "https://unauthorized-runtime.com/themes");
106+
107+
// The themeRoot should be set in the internal state
108+
cy.wrap({ getThemeRoot })
109+
.invoke("getThemeRoot")
110+
.should("equal", "https://unauthorized-runtime.com/themes");
111+
112+
// But validation should fail and log a warning
113+
cy.get("@consoleWarn").should("have.been.called");
114+
115+
// Verify link is NOT created in DOM
116+
cy.get("link[sap-ui-webcomponents-theme]")
117+
.should("not.exist");
118+
});
119+
});
120+
121+
describe("Relative themeRoot at runtime", () => {
122+
before(() => {
123+
cy.window()
124+
.then($el => {
125+
const metaTag = document.createElement("meta");
126+
metaTag.name = "sap-allowed-theme-origins";
127+
metaTag.content = "*";
128+
$el.document.head.append(metaTag);
129+
});
130+
});
131+
132+
after(() => {
133+
cy.window()
134+
.then($el => {
135+
const metaTag = $el.document.head.querySelector("[name='sap-allowed-theme-origins']");
136+
metaTag?.remove();
137+
const link = $el.document.head.querySelector("link[sap-ui-webcomponents-theme]");
138+
link?.remove();
139+
});
140+
});
141+
142+
it("should set raw relative path", () => {
143+
cy.wrap({ setThemeRoot })
144+
.invoke("setThemeRoot", "./custom-themes");
145+
146+
cy.wrap({ getThemeRoot })
147+
.invoke("getThemeRoot")
148+
.should("equal", "./custom-themes");
149+
150+
// Verify link is created with resolved URL
151+
cy.wrap({ getTheme })
152+
.invoke("getTheme")
153+
.then(theme => {
154+
cy.get(`link[sap-ui-webcomponents-theme="${theme}"]`)
155+
.should("exist")
156+
.and("have.attr", "href")
157+
.then(href => {
158+
return href.includes("/custom-themes/UI5/Base/baseLib/");
159+
})
160+
.should("be.true");
161+
});
162+
});
163+
});
164+
165+
describe("Same themeRoot not re-applied", () => {
166+
before(() => {
167+
cy.window()
168+
.then($el => {
169+
const metaTag = document.createElement("meta");
170+
metaTag.name = "sap-allowed-theme-origins";
171+
metaTag.content = "https://same-root.com";
172+
$el.document.head.append(metaTag);
173+
});
174+
});
175+
176+
after(() => {
177+
cy.window()
178+
.then($el => {
179+
const metaTag = $el.document.head.querySelector("[name='sap-allowed-theme-origins']");
180+
metaTag?.remove();
181+
const link = $el.document.head.querySelector("link[sap-ui-webcomponents-theme]");
182+
link?.remove();
183+
});
184+
});
185+
186+
it("should not reprocess when setting the same themeRoot", () => {
187+
const themeRoot = "https://same-root.com/themes";
188+
189+
cy.wrap({ setThemeRoot })
190+
.invoke("setThemeRoot", themeRoot)
191+
.should("not.equal", undefined);
192+
193+
// Setting again should return undefined (no-op)
194+
cy.wrap({ setThemeRoot })
195+
.invoke("setThemeRoot", themeRoot)
196+
.should("equal", undefined);
197+
});
198+
});
199+
200+
describe("ThemeRoot with wildcard origin", () => {
201+
before(() => {
202+
cy.window()
203+
.then($el => {
204+
const metaTag = document.createElement("meta");
205+
metaTag.name = "sap-allowed-theme-origins";
206+
metaTag.content = "*";
207+
$el.document.head.append(metaTag);
208+
});
209+
});
210+
211+
after(() => {
212+
cy.window()
213+
.then($el => {
214+
const metaTag = $el.document.head.querySelector("[name='sap-allowed-theme-origins']");
215+
metaTag?.remove();
216+
const link = $el.document.head.querySelector("link[sap-ui-webcomponents-theme]");
217+
link?.remove();
218+
});
219+
});
220+
221+
it("should allow any origin with wildcard", () => {
222+
cy.wrap({ setThemeRoot })
223+
.invoke("setThemeRoot", "https://any-wildcard-domain.com/themes");
224+
225+
cy.wrap({ getThemeRoot })
226+
.invoke("getThemeRoot")
227+
.should("equal", "https://any-wildcard-domain.com/themes");
228+
229+
// Verify link is created with wildcard-allowed origin
230+
cy.wrap({ getTheme })
231+
.invoke("getTheme")
232+
.then(theme => {
233+
cy.get(`link[sap-ui-webcomponents-theme="${theme}"]`)
234+
.should("exist")
235+
.and("have.attr", "href")
236+
.and("include", "https://any-wildcard-domain.com/themes/UI5/Base/baseLib/");
237+
});
238+
});
239+
});
240+
});

0 commit comments

Comments
 (0)