umb-localize encode HTML arguments (#18960)

* Moves `escapeHTML` call from localization controller to `umb-localize` element

* Adds supporting unit-test

* Removed unit-test

as it is now expected that the localization
controller will return literal HTML markup.

* Updated import path

* Removed extra call to `text()`
This commit is contained in:
Lee Kelleher
2025-04-08 13:32:36 +01:00
committed by GitHub
parent 86e7343f99
commit 134c8006c0
5 changed files with 22 additions and 19 deletions

View File

@@ -175,12 +175,6 @@ describe('UmbLocalizeController', () => {
expect((controller.term as any)('logout', 'Hello', 'World')).to.equal('Log out');
});
it('should encode HTML entities', () => {
expect(controller.term('withInlineToken', 'Hello', '<script>alert("XSS")</script>'), 'XSS detected').to.equal(
'Hello &lt;script&gt;alert(&#34;XSS&#34;)&lt;/script&gt;',
);
});
it('only reacts to changes of its own localization-keys', async () => {
const element: UmbLocalizationRenderCountElement = await fixture(
html`<umb-localization-render-count></umb-localization-render-count>`,

View File

@@ -20,7 +20,6 @@ import type {
import { umbLocalizationManager } from './localization.manager.js';
import type { LitElement } from '@umbraco-cms/backoffice/external/lit';
import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { escapeHTML } from '@umbraco-cms/backoffice/utils';
const LocalizationControllerAlias = Symbol();
/**
@@ -137,20 +136,16 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
return String(key);
}
// As translated texts can contain HTML, we will need to render with unsafeHTML.
// But arguments can come from user input, so they should be escaped.
const sanitizedArgs = args.map((a) => escapeHTML(a));
if (typeof term === 'function') {
return term(...sanitizedArgs) as string;
return term(...args) as string;
}
if (typeof term === 'string') {
if (sanitizedArgs.length) {
if (args.length) {
// Replace placeholders of format "%index%" and "{index}" with provided values
term = term.replace(/(%(\d+)%|\{(\d+)\})/g, (match, _p1, p2, p3): string => {
const index = p2 || p3;
return typeof sanitizedArgs[index] !== 'undefined' ? String(sanitizedArgs[index]) : match;
return typeof args[index] !== 'undefined' ? String(args[index]) : match;
});
}
}

View File

@@ -95,6 +95,14 @@ describe('umb-localize', () => {
expect(element.shadowRoot?.innerHTML).to.contain('Hello World');
});
it('should localize a key with multiple arguments as encoded HTML', async () => {
element.key = 'general_moreThanOneArgument';
element.args = ['<strong>Hello</strong>', '<em>World</em>'];
await elementUpdated(element);
expect(element.shadowRoot?.innerHTML).to.contain('&lt;strong&gt;Hello&lt;/strong&gt; &lt;em&gt;World&lt;/em&gt;');
});
it('should localize a key with args as an attribute', async () => {
element.key = 'general_moreThanOneArgument';
element.setAttribute('args', '["Hello","World"]');

View File

@@ -1,4 +1,5 @@
import { css, customElement, html, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
import { escapeHTML } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
/**
@@ -34,7 +35,11 @@ export class UmbLocalizeElement extends UmbLitElement {
@state()
protected get text(): string {
const localizedValue = this.localize.term(this.key, ...(this.args ?? []));
// As translated texts can contain HTML, we will need to render with unsafeHTML.
// But arguments can come from user input, so they should be escaped.
const escapedArgs = (this.args ?? []).map((a) => escapeHTML(a));
const localizedValue = this.localize.term(this.key, ...escapedArgs);
// If the value is the same as the key, it means the key was not found.
if (localizedValue === this.key) {
@@ -44,12 +49,13 @@ export class UmbLocalizeElement extends UmbLitElement {
(this.getHostElement() as HTMLElement).removeAttribute('data-localize-missing');
return localizedValue;
return localizedValue.trim();
}
override render() {
return this.text.trim()
? html`${unsafeHTML(this.text)}`
const text = this.text;
return text
? unsafeHTML(text)
: this.debug
? html`<span style="color:red">${this.key}</span>`
: html`<slot></slot>`;

View File

@@ -4,7 +4,7 @@ const NON_ALPHANUMERIC_REGEXP = /([^#-~| |!])/g;
/**
* Escapes HTML entities in a string.
* @example escapeHTML('<script>alert("XSS")</script>'), // "&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;"
* @example escapeHTML('<script>alert("XSS")</script>'), // "&lt;script&gt;alert(&#34;XSS&#34;)&lt;/script&gt;"
* @param html The HTML string to escape.
* @returns The sanitized HTML string.
*/