From 68b1b98c83fd39c566601ba246eada0088f72d45 Mon Sep 17 00:00:00 2001
From: David Olson <157068235+DavidOlson-Hyland@users.noreply.github.com>
Date: Mon, 9 Mar 2026 10:25:55 -0500
Subject: [PATCH] AAE-42904 Escape html in evaluated fields and variables for
rich-text display widget (#11718)
---
.../base-display-text.widget.ts | 4 +--
.../services/form-expression.service.spec.ts | 28 +++++++++++++++++++
.../form/services/form-expression.service.ts | 15 +++++++++-
.../display-rich-text.widget.spec.ts | 4 ++-
.../display-rich-text.widget.ts | 2 +-
5 files changed, 48 insertions(+), 5 deletions(-)
diff --git a/lib/core/src/lib/form/components/widgets/base-display-text/base-display-text.widget.ts b/lib/core/src/lib/form/components/widgets/base-display-text/base-display-text.widget.ts
index 0871f39f15..b3a6e93bea 100644
--- a/lib/core/src/lib/form/components/widgets/base-display-text/base-display-text.widget.ts
+++ b/lib/core/src/lib/form/components/widgets/base-display-text/base-display-text.widget.ts
@@ -68,8 +68,8 @@ export abstract class BaseDisplayTextWidgetComponent extends WidgetComponent imp
protected abstract reevaluateExpressions(): void;
private readonly destroyRef = inject(DestroyRef);
- protected resolveExpressions(text: string): string {
- return this.formExpressionService.resolveExpressions(this.field.form, text);
+ protected resolveExpressions(text: string, escapeHtml?: boolean): string {
+ return this.formExpressionService.resolveExpressions(this.field.form, text, escapeHtml);
}
private applyExpressions() {
diff --git a/lib/core/src/lib/form/services/form-expression.service.spec.ts b/lib/core/src/lib/form/services/form-expression.service.spec.ts
index ffd8eb1948..0758ee8d6c 100644
--- a/lib/core/src/lib/form/services/form-expression.service.spec.ts
+++ b/lib/core/src/lib/form/services/form-expression.service.spec.ts
@@ -268,6 +268,34 @@ describe('FormExpressionService', () => {
expect(result).toBe('true');
});
+
+ describe('when escapeHtml is true', () => {
+ let getFieldSpy: jasmine.Spy;
+
+ beforeEach(() => {
+ getFieldSpy = spyOn(formModel, 'getFieldById');
+ });
+
+ it('should escape ampersands in resolved field values', () => {
+ getFieldSpy.and.returnValue({ id: 'f', value: 'a & b' } as any);
+ expect(service.resolveExpressions(formModel, '${field.f}', true)).toBe('a & b');
+ });
+
+ it('should escape all HTML special characters in an XSS-like payload', () => {
+ getFieldSpy.and.returnValue({ id: 'f', value: `
` } as any);
+ expect(service.resolveExpressions(formModel, '${field.f}', true)).toBe('<img src="x" onerror='alert(1)'>');
+ });
+
+ it('should not escape characters when escapeHtml is false', () => {
+ getFieldSpy.and.returnValue({ id: 'f', value: 'bold' } as any);
+ expect(service.resolveExpressions(formModel, '${field.f}', false)).toBe('bold');
+ });
+
+ it('should not escape characters when escapeHtml is omitted', () => {
+ getFieldSpy.and.returnValue({ id: 'f', value: 'bold' } as any);
+ expect(service.resolveExpressions(formModel, '${field.f}')).toBe('bold');
+ });
+ });
});
describe('getFieldDependencies', () => {
diff --git a/lib/core/src/lib/form/services/form-expression.service.ts b/lib/core/src/lib/form/services/form-expression.service.ts
index 706b4ed615..01faf4669c 100644
--- a/lib/core/src/lib/form/services/form-expression.service.ts
+++ b/lib/core/src/lib/form/services/form-expression.service.ts
@@ -27,7 +27,7 @@ export class FormExpressionService {
private readonly VARIABLE_PREFIX = 'variable.';
private readonly VARIABLES_REGEX = /(?:field|variable)\.[a-zA-Z_$][a-zA-Z0-9_$]*/g;
- resolveExpressions(form: FormModel, formField: string): string {
+ resolveExpressions(form: FormModel, formField: string, escapeHtml?: boolean): string {
let result = formField || '';
const matches = result.match(this.GLOBAL_EXPRESSION_REGEX);
@@ -43,6 +43,19 @@ export class FormExpressionService {
} else if (typeof expressionResult !== 'string') {
expressionResult = JSON.stringify(expressionResult);
}
+ if (escapeHtml) {
+ expressionResult = expressionResult
+ .split('&')
+ .join('&')
+ .split('<')
+ .join('<')
+ .split('>')
+ .join('>')
+ .split('"')
+ .join('"')
+ .split("'")
+ .join(''');
+ }
result = result.replace(match, expressionResult);
}
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts
index 74a0d23e72..414b4ca337 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.spec.ts
@@ -137,13 +137,15 @@ describe('DisplayRichTextWidgetComponent', () => {
});
it('should sanitize unsafe HTML', async () => {
+ mockRichTextParserService.parse.and.returnValue('
');
widget.field = mockUnsafeFormField;
fixture.detectChanges();
await fixture.whenStable();
const parsedHtmlEl = debugEl.query(By.css(cssSelector.parsedHTML));
- expect(parsedHtmlEl.nativeElement.innerHTML.includes('
')).toBe(false);
+ expect(parsedHtmlEl.nativeElement.innerHTML.includes('img src="x"')).toBe(true);
+ expect(parsedHtmlEl.nativeElement.innerHTML.includes('onerror')).toBe(false);
});
describe('expression evaluation', () => {
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts
index e6413b76f2..cf6f4a6193 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-rich-text/display-rich-text.widget.ts
@@ -90,7 +90,7 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
private applyExpressionsToBlocks(value: any): void {
for (const block of value.blocks) {
- block.data.text = this.resolveExpressions(block.data.text);
+ block.data.text = this.resolveExpressions(block.data.text, true);
}
this.field.value = value;
}