Skip to content

Commit d4f8754

Browse files
Merge commit from fork
* Escape HTML in strings provided as arguments for client-side localization. * Used HTML sanitizer from dependency. * Moved sanitization to controller. * Fixed path to dependency. * fix: move constants to their own respective files to avoid circular dependencies * fix: import through import map * fix: import `sanitizeHTML` from importmap * chore: consts * feat: add more ops/sec to `term` function * fix: check specifically for `typeof 'undefined'` before determining if an arg is valid or not in case where the sanitizer removes everything so the arg is just an empty string (`""`) we still want it to replace the token, i.e. with the empty string * test: add tests to verify localization controller xss * chore: try to improve ops/sec and readability of the `term` function * test: add more tests for the sanitizer * fix(dictionary workspace): rely on localization sanitization * feat: adds new function to escape html entities * fix: use `escapeHTML` to escape/encode only HTML entities * test: update test to reflect correct escape * test: add tests for sanitizeHTML and escapeHTML * fix: update function to not escape all quotes as people may want to keep those * chore: formatting --------- Co-authored-by: Jacob Overgaard <[email protected]>
1 parent df03581 commit d4f8754

16 files changed

+96
-23
lines changed

src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ describe('UmbLocalizeController', () => {
175175
expect((controller.term as any)('logout', 'Hello', 'World')).to.equal('Log out');
176176
});
177177

178+
it('should encode HTML entities', () => {
179+
expect(controller.term('withInlineToken', 'Hello', '<script>alert("XSS")</script>'), 'XSS detected').to.equal(
180+
'Hello &lt;script&gt;alert(&#34;XSS&#34;)&lt;/script&gt;',
181+
);
182+
});
183+
178184
it('only reacts to changes of its own localization-keys', async () => {
179185
const element: UmbLocalizationRenderCountElement = await fixture(
180186
html`<umb-localization-render-count></umb-localization-render-count>`,

src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
import { umbLocalizationManager } from './localization.manager.js';
2121
import type { LitElement } from '@umbraco-cms/backoffice/external/lit';
2222
import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
23+
import { escapeHTML } from '@umbraco-cms/backoffice/utils';
2324

2425
const LocalizationControllerAlias = Symbol();
2526
/**
@@ -119,29 +120,35 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
119120
}
120121

121122
const { primary, secondary } = this.getLocalizationData(this.lang());
123+
124+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
122125
let term: any;
123126

124127
// Look for a matching term using regionCode, code, then the fallback
125-
if (primary && primary[key]) {
128+
if (primary?.[key]) {
126129
term = primary[key];
127-
} else if (secondary && secondary[key]) {
130+
} else if (secondary?.[key]) {
128131
term = secondary[key];
129-
} else if (umbLocalizationManager.fallback && umbLocalizationManager.fallback[key]) {
132+
} else if (umbLocalizationManager.fallback?.[key]) {
130133
term = umbLocalizationManager.fallback[key];
131134
} else {
132135
return String(key);
133136
}
134137

138+
// As translated texts can contain HTML, we will need to render with unsafeHTML.
139+
// But arguments can come from user input, so they should be escaped.
140+
const sanitizedArgs = args.map((a) => escapeHTML(a));
141+
135142
if (typeof term === 'function') {
136-
return term(...args) as string;
143+
return term(...sanitizedArgs) as string;
137144
}
138145

139146
if (typeof term === 'string') {
140-
if (args.length > 0) {
147+
if (sanitizedArgs.length) {
141148
// Replace placeholders of format "%index%" and "{index}" with provided values
142149
term = term.replace(/(%(\d+)%|\{(\d+)\})/g, (match, _p1, p2, p3): string => {
143150
const index = p2 || p3;
144-
return String(args[index] || match);
151+
return typeof sanitizedArgs[index] !== 'undefined' ? String(sanitizedArgs[index]) : match;
145152
});
146153
}
147154
}

src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* License for the specific language governing permissions and limitations under
1414
* the License.
1515
*/
16-
import { UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js';
16+
import { UMB_STORAGE_TOKEN_RESPONSE_NAME } from './constants.js';
1717
import type { LocationLike, StringMap } from '@umbraco-cms/backoffice/external/openid';
1818
import {
1919
BaseTokenRequestHandler,

src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.token.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,3 @@ import type { UmbAuthContext } from './auth.context.js';
22
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
33

44
export const UMB_AUTH_CONTEXT = new UmbContextToken<UmbAuthContext>('UmbAuthContext');
5-
export const UMB_STORAGE_TOKEN_RESPONSE_NAME = 'umb:userAuthTokenResponse';
6-
export const UMB_STORAGE_REDIRECT_URL = 'umb:auth:redirect';

src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { UmbAuthFlow } from './auth-flow.js';
2-
import { UMB_AUTH_CONTEXT, UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js';
2+
import { UMB_AUTH_CONTEXT } from './auth.context.token.js';
33
import type { UmbOpenApiConfiguration } from './models/openApiConfiguration.js';
44
import type { ManifestAuthProvider } from './auth-provider.extension.js';
5+
import { UMB_STORAGE_TOKEN_RESPONSE_NAME } from './constants.js';
56
import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api';
67
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
78
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';

src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import type { UmbAuthProviderDefaultProps, UmbUserLoginState } from '../types.js';
2-
import { UmbLitElement } from '../../lit-element/lit-element.element.js';
3-
import { UmbTextStyles } from '../../style/index.js';
42
import type { ManifestAuthProvider } from '../auth-provider.extension.js';
53
import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit';
4+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
5+
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
66

77
@customElement('umb-auth-provider-default')
88
export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbAuthProviderDefaultProps {
99
@property({ attribute: false })
1010
userLoginState?: UmbUserLoginState | undefined;
11+
1112
@property({ attribute: false })
1213
manifest!: ManifestAuthProvider;
14+
1315
@property({ attribute: false })
1416
onSubmit!: (manifestOrProviderName: string | ManifestAuthProvider, loginHint?: string) => void;
1517

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const UMB_STORAGE_TOKEN_RESPONSE_NAME = 'umb:userAuthTokenResponse';

src/Umbraco.Web.UI.Client/src/packages/core/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import './components/index.js';
22

33
export * from './auth.context.js';
44
export * from './auth.context.token.js';
5+
export * from './constants.js';
56
export * from './modals/index.js';
67
export type * from './models/openApiConfiguration.js';
78
export * from './components/index.js';

src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from './path/stored-path.function.js';
1919
export * from './path/transform-server-path-to-client-path.function.js';
2020
export * from './path/umbraco-path.function.js';
2121
export * from './path/url-pattern-to-string.function.js';
22+
export * from './sanitize/escape-html.function.js';
2223
export * from './sanitize/sanitize-html.function.js';
2324
export * from './selection-manager/selection.manager.js';
2425
export * from './state-manager/index.js';

src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { retrieveStoredPath, setStoredPath } from './stored-path.function.js';
1+
import { retrieveStoredPath, setStoredPath, UMB_STORAGE_REDIRECT_URL } from './stored-path.function.js';
22
import { expect } from '@open-wc/testing';
3-
import { UMB_STORAGE_REDIRECT_URL } from '@umbraco-cms/backoffice/auth';
43

54
describe('retrieveStoredPath', () => {
65
beforeEach(() => {

0 commit comments

Comments
 (0)