Skip to content

Commit 709b480

Browse files
authored
Sanitize tooltip content (#438)
1 parent 66a0f62 commit 709b480

File tree

4 files changed

+127
-13
lines changed

4 files changed

+127
-13
lines changed

cypress/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"types": ["cypress", "node"],
66
"resolveJsonModule": true
77
},
8-
"include": ["**/*.ts"]
8+
"include": ["**/*.ts", "../cypress.config.ts"]
99
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {
2+
ComponentFixture,
3+
fakeAsync,
4+
TestBed,
5+
tick
6+
} from '@angular/core/testing';
7+
import { CpsTooltipDirective } from './cps-tooltip.directive';
8+
import { Component } from '@angular/core';
9+
import { By } from '@angular/platform-browser';
10+
11+
@Component({
12+
template: `<div cpsTooltip="<style onload='alert(420);'></style>"></div>`,
13+
imports: [CpsTooltipDirective]
14+
})
15+
class MaliciousTooltipComponent {}
16+
17+
@Component({
18+
template: `<div cpsTooltip="<h1>Legit tooltip</h1>"></div>`,
19+
imports: [CpsTooltipDirective]
20+
})
21+
class LegitTooltipComponent {}
22+
23+
describe('CpsTooltipDirective', () => {
24+
let legitComponent: LegitTooltipComponent;
25+
let legitComponentFixture: ComponentFixture<LegitTooltipComponent>;
26+
27+
let maliciousComponent: MaliciousTooltipComponent;
28+
let maliciousComponentFixture: ComponentFixture<MaliciousTooltipComponent>;
29+
30+
beforeEach(async () => {
31+
await TestBed.configureTestingModule({
32+
imports: []
33+
}).compileComponents();
34+
});
35+
36+
beforeEach(() => {
37+
legitComponentFixture = TestBed.createComponent(LegitTooltipComponent);
38+
legitComponent = legitComponentFixture.componentInstance;
39+
legitComponentFixture.detectChanges();
40+
41+
maliciousComponentFixture = TestBed.createComponent(
42+
MaliciousTooltipComponent
43+
);
44+
maliciousComponent = maliciousComponentFixture.componentInstance;
45+
maliciousComponentFixture.detectChanges();
46+
});
47+
48+
it('should create the component', () => {
49+
expect(maliciousComponent).toBeTruthy();
50+
expect(legitComponent).toBeTruthy();
51+
});
52+
53+
it('should sanitize the malicious tooltip content', fakeAsync(() => {
54+
const consoleWarnSpy = jest
55+
.spyOn(console, 'warn')
56+
.mockImplementation(() => {});
57+
58+
const divElement = maliciousComponentFixture.debugElement.query(
59+
By.css('div')
60+
);
61+
divElement.triggerEventHandler('mouseenter', null);
62+
maliciousComponentFixture.detectChanges();
63+
64+
tick(300);
65+
66+
const tooltipElement: HTMLElement | null =
67+
document.body.querySelector('.cps-tooltip');
68+
69+
expect(tooltipElement).toBeTruthy();
70+
expect(tooltipElement?.innerHTML).toBe(
71+
'<div class="cps-tooltip-content">Add your text to this tooltip</div>'
72+
);
73+
// Angular informs about stripping some content during sanitization
74+
expect(consoleWarnSpy).toHaveBeenCalledWith(
75+
expect.stringContaining('sanitizing HTML stripped some content')
76+
);
77+
78+
divElement.triggerEventHandler('mouseleave', null);
79+
maliciousComponentFixture.detectChanges();
80+
81+
tick(500);
82+
83+
expect(document.body.querySelector('.cps-tooltip')).toBeFalsy();
84+
}));
85+
86+
it('should properly show legit tooltip', fakeAsync(() => {
87+
const divElement = legitComponentFixture.debugElement.query(By.css('div'));
88+
divElement.triggerEventHandler('mouseenter', null);
89+
legitComponentFixture.detectChanges();
90+
91+
tick(300);
92+
93+
const tooltipElement: HTMLElement | null =
94+
document.body.querySelector('.cps-tooltip');
95+
96+
expect(tooltipElement).toBeTruthy();
97+
expect(tooltipElement?.innerHTML).toBe(
98+
'<div class="cps-tooltip-content"><h1>Legit tooltip</h1></div>'
99+
);
100+
101+
divElement.triggerEventHandler('mouseleave', null);
102+
legitComponentFixture.detectChanges();
103+
104+
tick(500);
105+
106+
expect(document.body.querySelector('.cps-tooltip')).toBeFalsy();
107+
}));
108+
});

projects/cps-ui-kit/src/lib/directives/cps-tooltip/cps-tooltip.directive.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import {
22
Directive,
33
ElementRef,
44
HostListener,
5-
Inject,
5+
inject,
66
Input,
7-
OnDestroy
7+
OnDestroy,
8+
SecurityContext
89
} from '@angular/core';
910
import { convertSize } from '../../utils/internal/size-utils';
1011
import { DOCUMENT } from '@angular/common';
12+
import { DomSanitizer } from '@angular/platform-browser';
1113

1214
/**
1315
* CpsTooltipPosition is used to define the position of the tooltip.
@@ -96,11 +98,12 @@ export class CpsTooltipDirective implements OnDestroy {
9698

9799
private window: Window;
98100

99-
constructor(
100-
private _elementRef: ElementRef<HTMLElement>,
101-
@Inject(DOCUMENT) private document: Document
102-
) {
103-
this.window = this.document.defaultView as Window;
101+
private _elementRef = inject(ElementRef<HTMLElement>);
102+
private _document = inject(DOCUMENT);
103+
private _domSanitizer = inject(DomSanitizer);
104+
105+
constructor() {
106+
this.window = this._document.defaultView as Window;
104107
}
105108

106109
ngOnDestroy(): void {
@@ -112,7 +115,7 @@ export class CpsTooltipDirective implements OnDestroy {
112115

113116
if (this.tooltipDisabled) return;
114117

115-
this._popup = this.document.createElement('div');
118+
this._popup = this._document.createElement('div');
116119
this._constructElement(this._popup);
117120

118121
if (this.tooltipPersistent)
@@ -149,15 +152,17 @@ export class CpsTooltipDirective implements OnDestroy {
149152
};
150153

151154
private _constructElement(popup: HTMLDivElement) {
152-
const popupContent = this.document.createElement('div');
153-
popupContent.innerHTML = this.tooltip || 'Add your text to this tooltip';
155+
const popupContent = this._document.createElement('div');
156+
popupContent.innerHTML =
157+
this._domSanitizer.sanitize(SecurityContext.HTML, this.tooltip) ||
158+
'Add your text to this tooltip';
154159
popupContent.classList.add(this.tooltipContentClass);
155160
popup.appendChild(popupContent);
156161

157162
popup.classList.add('cps-tooltip');
158163
popup.style.maxWidth = convertSize(this.tooltipMaxWidth);
159164

160-
this.document.body.appendChild(popup);
165+
this._document.body.appendChild(popup);
161166
requestAnimationFrame(function () {
162167
popup.style.opacity = '1';
163168
});

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
"strictInjectionParameters": true,
3131
"strictInputAccessModifiers": true,
3232
"strictTemplates": true
33-
}
33+
},
34+
"exclude": ["cypress/**/*.ts", "cypress.config.ts"]
3435
}

0 commit comments

Comments
 (0)