mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-42904 Escape html in evaluated fields and variables for rich-text display widget (#11718)
This commit is contained in:
+2
-2
@@ -68,8 +68,8 @@ export abstract class BaseDisplayTextWidgetComponent extends WidgetComponent imp
|
|||||||
protected abstract reevaluateExpressions(): void;
|
protected abstract reevaluateExpressions(): void;
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
protected resolveExpressions(text: string): string {
|
protected resolveExpressions(text: string, escapeHtml?: boolean): string {
|
||||||
return this.formExpressionService.resolveExpressions(this.field.form, text);
|
return this.formExpressionService.resolveExpressions(this.field.form, text, escapeHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyExpressions() {
|
private applyExpressions() {
|
||||||
|
|||||||
@@ -268,6 +268,34 @@ describe('FormExpressionService', () => {
|
|||||||
|
|
||||||
expect(result).toBe('true');
|
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: `<img src="x" onerror='alert(1)'>` } 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: '<b>bold</b>' } as any);
|
||||||
|
expect(service.resolveExpressions(formModel, '${field.f}', false)).toBe('<b>bold</b>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not escape characters when escapeHtml is omitted', () => {
|
||||||
|
getFieldSpy.and.returnValue({ id: 'f', value: '<b>bold</b>' } as any);
|
||||||
|
expect(service.resolveExpressions(formModel, '${field.f}')).toBe('<b>bold</b>');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getFieldDependencies', () => {
|
describe('getFieldDependencies', () => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class FormExpressionService {
|
|||||||
private readonly VARIABLE_PREFIX = 'variable.';
|
private readonly VARIABLE_PREFIX = 'variable.';
|
||||||
private readonly VARIABLES_REGEX = /(?:field|variable)\.[a-zA-Z_$][a-zA-Z0-9_$]*/g;
|
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 || '';
|
let result = formField || '';
|
||||||
|
|
||||||
const matches = result.match(this.GLOBAL_EXPRESSION_REGEX);
|
const matches = result.match(this.GLOBAL_EXPRESSION_REGEX);
|
||||||
@@ -43,6 +43,19 @@ export class FormExpressionService {
|
|||||||
} else if (typeof expressionResult !== 'string') {
|
} else if (typeof expressionResult !== 'string') {
|
||||||
expressionResult = JSON.stringify(expressionResult);
|
expressionResult = JSON.stringify(expressionResult);
|
||||||
}
|
}
|
||||||
|
if (escapeHtml) {
|
||||||
|
expressionResult = expressionResult
|
||||||
|
.split('&')
|
||||||
|
.join('&')
|
||||||
|
.split('<')
|
||||||
|
.join('<')
|
||||||
|
.split('>')
|
||||||
|
.join('>')
|
||||||
|
.split('"')
|
||||||
|
.join('"')
|
||||||
|
.split("'")
|
||||||
|
.join(''');
|
||||||
|
}
|
||||||
result = result.replace(match, expressionResult);
|
result = result.replace(match, expressionResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -137,13 +137,15 @@ describe('DisplayRichTextWidgetComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sanitize unsafe HTML', async () => {
|
it('should sanitize unsafe HTML', async () => {
|
||||||
|
mockRichTextParserService.parse.and.returnValue('<img src="x" onerror="alert(\'XSS\')">');
|
||||||
widget.field = mockUnsafeFormField;
|
widget.field = mockUnsafeFormField;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
|
|
||||||
const parsedHtmlEl = debugEl.query(By.css(cssSelector.parsedHTML));
|
const parsedHtmlEl = debugEl.query(By.css(cssSelector.parsedHTML));
|
||||||
expect(parsedHtmlEl.nativeElement.innerHTML.includes('<img src="x" onerror="alert(\'XSS\')">')).toBe(false);
|
expect(parsedHtmlEl.nativeElement.innerHTML.includes('img src="x"')).toBe(true);
|
||||||
|
expect(parsedHtmlEl.nativeElement.innerHTML.includes('onerror')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('expression evaluation', () => {
|
describe('expression evaluation', () => {
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
|
|||||||
|
|
||||||
private applyExpressionsToBlocks(value: any): void {
|
private applyExpressionsToBlocks(value: any): void {
|
||||||
for (const block of value.blocks) {
|
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;
|
this.field.value = value;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user