Skip to content

Commit cda3c34

Browse files
mattjokeHejdaJakub
authored andcommitted
feat(admin): HTML escape in notifications and application forms
* With incorrect input the HTML content is not accepted and the user is notified * The HTML content is escaped before it is sent to Perun * the HTML content is escaped in the email footer as well
1 parent 079f927 commit cda3c34

15 files changed

+480
-61
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"angular.enable-strict-mode-prompt": false
3+
}

apps/admin-gui/src/app/shared/components/dialogs/add-edit-notification-dialog/add-edit-notification-dialog.component.html

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ <h1 mat-dialog-title>{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.TITLE_EDIT' | transl
114114
</mat-tab>
115115
<mat-tab
116116
*ngFor="let lang of languages"
117-
label="{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.LANG_' + lang | uppercase | translate}}">
117+
label="{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.LANG_' + lang | uppercase | translate}}"
118+
[formGroup]="inputFormGroup">
118119
<ng-template matTabContent>
119120
<perun-web-apps-alert alert_type="info" *ngIf="htmlAuth">
120121
<i [innerHTML]="'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.FORMAT_INFO' | translate"></i>
@@ -142,42 +143,48 @@ <h1 mat-dialog-title>{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.TITLE_EDIT' | transl
142143
<span class="fw-bold pe-2">
143144
{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.SUBJECT' | translate}}:
144145
</span>
145-
<mat-form-field class="w-100">
146+
<mat-form-field class="w-100" subscriptSizing="dynamic">
146147
<div #Input>
147148
<input
148149
*ngIf="format === 'plain_text'"
149-
[(ngModel)]="this.applicationMail.message[lang].subject"
150+
formControlName="{{lang}}-plain-subject"
150151
(focus)="isTextFocused = false"
151152
matInput />
152153
<input
153154
*ngIf="format === 'html'"
154-
[(ngModel)]="this.applicationMail.htmlMessage[lang].subject"
155+
formControlName="{{lang}}-html-subject"
155156
(focus)="isTextFocused = false"
156157
matInput />
157158
</div>
159+
<mat-error *ngIf="inputFormGroup.controls[lang + '-html-subject']?.invalid">
160+
{{inputFormGroup.controls[lang + '-html-subject'].errors?.invalidHtmlContent}}
161+
</mat-error>
158162
</mat-form-field>
159163
</div>
160164
<div class="fw-bold">
161165
{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.TEXT' | translate}}:
162166
</div>
163167

164-
<mat-form-field class="w-100">
168+
<mat-form-field class="w-100" subscriptSizing="dynamic">
165169
<div #Textarea>
166170
<textarea
167171
*ngIf="format === 'plain_text'"
168-
[(ngModel)]="this.applicationMail.message[lang].text"
169172
matInput
173+
formControlName="{{lang}}-plain-text"
170174
(focus)="isTextFocused = true"
171175
rows="17">
172176
</textarea>
173177
<textarea
174178
*ngIf="format === 'html'"
175-
[(ngModel)]="this.applicationMail.htmlMessage[lang].text"
179+
formControlName="{{lang}}-html-text"
176180
matInput
177181
(focus)="isTextFocused = true"
178182
rows="17">
179183
</textarea>
180184
</div>
185+
<mat-error *ngIf="inputFormGroup.controls[lang + '-html-text']?.invalid">
186+
{{inputFormGroup.controls[lang + '-html-text'].errors?.invalidHtmlContent}}
187+
</mat-error>
181188
</mat-form-field>
182189
</div>
183190

@@ -194,7 +201,6 @@ <h1 mat-dialog-title>{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.TITLE_EDIT' | transl
194201
</mat-tab>
195202
</mat-tab-group>
196203
</div>
197-
198204
<div class="d-flex mt-auto" mat-dialog-actions>
199205
<button (click)="cancel()" class="ms-auto" mat-flat-button>
200206
{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.CANCEL_BUTTON' | translate}}
@@ -204,23 +210,28 @@ <h1 mat-dialog-title>{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.TITLE_EDIT' | transl
204210
*ngIf="data.createMailNotification"
205211
class="ms-2"
206212
color="accent"
207-
[disabled]="invalidNotification"
213+
[disabled]="invalidNotification || inputFormGroup.invalid"
208214
mat-flat-button>
209215
{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.CREATE_BUTTON' | translate}}
210216
</button>
211217
<div
212218
[matTooltipDisabled]="editAuth"
213219
[matTooltipPosition]="'above'"
214220
matTooltip="{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.EDIT_HINT' | translate}}">
215-
<button
216-
(click)="save()"
217-
*ngIf="!data.createMailNotification"
218-
class="ms-2"
219-
color="accent"
220-
[disabled]="!editAuth"
221-
mat-flat-button>
222-
{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.SAVE_BUTTON' | translate}}
223-
</button>
221+
<div
222+
[matTooltipDisabled]="!inputFormGroup.invalid"
223+
[matTooltipPosition]="'above'"
224+
matTooltip="{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.INVALID_HTML_TAGS' | translate}}">
225+
<button
226+
(click)="save()"
227+
*ngIf="!data.createMailNotification"
228+
class="ms-2"
229+
color="accent"
230+
[disabled]="!editAuth || inputFormGroup.invalid"
231+
mat-flat-button>
232+
{{'DIALOGS.NOTIFICATIONS_ADD_EDIT_MAIL.SAVE_BUTTON' | translate}}
233+
</button>
234+
</div>
224235
</div>
225236
</div>
226237
</div>

apps/admin-gui/src/app/shared/components/dialogs/add-edit-notification-dialog/add-edit-notification-dialog.component.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@
66
.disabled-label {
77
color: rgba(0, 0, 0, 0.38) !important;
88
}
9+
10+
::ng-deep .mat-mdc-form-field-error-wrapper {
11+
position: relative !important;
12+
word-break: break-word;
13+
}

apps/admin-gui/src/app/shared/components/dialogs/add-edit-notification-dialog/add-edit-notification-dialog.component.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { FormGroup, FormControl } from '@angular/forms';
12
import { Component, Inject, OnInit } from '@angular/core';
23
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
34
import { openClose, tagsOpenClose } from '@perun-web-apps/perun/animations';
@@ -6,7 +7,7 @@ import {
67
GroupsManagerService,
78
RegistrarManagerService,
89
} from '@perun-web-apps/perun/openapi';
9-
import { GuiAuthResolver, StoreService } from '@perun-web-apps/perun/services';
10+
import { GuiAuthResolver, StoreService, HtmlEscapeService } from '@perun-web-apps/perun/services';
1011

1112
export interface ApplicationFormAddEditMailDialogData {
1213
theme: string;
@@ -34,14 +35,16 @@ export class AddEditNotificationDialogComponent implements OnInit {
3435
languages = ['en'];
3536
formats = ['plain_text', 'html'];
3637
htmlAuth: boolean;
38+
inputFormGroup: FormGroup = null;
3739

3840
constructor(
3941
private dialogRef: MatDialogRef<AddEditNotificationDialogComponent>,
4042
private registrarService: RegistrarManagerService,
4143
@Inject(MAT_DIALOG_DATA) public data: ApplicationFormAddEditMailDialogData,
4244
private authResolver: GuiAuthResolver,
4345
private groupsService: GroupsManagerService,
44-
private store: StoreService
46+
private store: StoreService,
47+
private inputEscape: HtmlEscapeService
4548
) {}
4649

4750
ngOnInit(): void {
@@ -68,6 +71,31 @@ export class AddEditNotificationDialogComponent implements OnInit {
6871
'vo-addMail_ApplicationForm_ApplicationMail_policy',
6972
[vo]
7073
);
74+
75+
const formGroupFields: { [key: string]: FormControl } = {};
76+
for (const lang of this.languages) {
77+
// Plain
78+
formGroupFields[`${lang}-plain-subject`] = new FormControl(
79+
this.applicationMail.message[lang].subject,
80+
[]
81+
);
82+
formGroupFields[`${lang}-plain-text`] = new FormControl(
83+
this.applicationMail.message[lang].text,
84+
[]
85+
);
86+
// Html
87+
formGroupFields[`${lang}-html-subject`] = new FormControl(
88+
this.applicationMail.htmlMessage[lang].subject,
89+
[this.inputEscape.htmlContentValidator()]
90+
);
91+
formGroupFields[`${lang}-html-text`] = new FormControl(
92+
this.applicationMail.htmlMessage[lang].text,
93+
[this.inputEscape.htmlContentValidator()]
94+
);
95+
formGroupFields[`${lang}-html-subject`].markAsTouched();
96+
formGroupFields[`${lang}-html-text`].markAsTouched();
97+
}
98+
this.inputFormGroup = new FormGroup(formGroupFields);
7199
}
72100
}
73101

@@ -110,6 +138,26 @@ export class AddEditNotificationDialogComponent implements OnInit {
110138

111139
save(): void {
112140
this.loading = true;
141+
// Validate notification
142+
for (const lang of this.languages) {
143+
let escaped = this.inputEscape.escapeDangerousHtml(
144+
String(this.inputFormGroup.get(`${lang}-html-subject`).value)
145+
);
146+
this.applicationMail.htmlMessage[lang].subject = escaped.escapedHtml;
147+
escaped = this.inputEscape.escapeDangerousHtml(
148+
String(this.inputFormGroup.get(`${lang}-html-text`).value)
149+
);
150+
this.applicationMail.htmlMessage[lang].text = escaped.escapedHtml;
151+
152+
// Update application with content from FormControl
153+
this.applicationMail.message[lang].subject = String(
154+
this.inputFormGroup.get(`${lang}-plain-subject`).value
155+
);
156+
this.applicationMail.message[lang].text = String(
157+
this.inputFormGroup.get(`${lang}-plain-text`).value
158+
);
159+
}
160+
113161
this.registrarService.updateApplicationMail({ mail: this.applicationMail }).subscribe(
114162
() => {
115163
this.dialogRef.close(true);

apps/admin-gui/src/app/shared/components/dialogs/edit-application-form-item-dialog/edit-application-form-item-dialog.component.html

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,16 @@ <h1 mat-dialog-title>
175175
<app-edit-application-form-item-line
176176
[description]="'DIALOGS.APPLICATION_FORM_EDIT_ITEM.CONTENT_DESCRIPTION' | translate"
177177
[label]="'DIALOGS.APPLICATION_FORM_EDIT_ITEM.CONTENT' | translate">
178-
<mat-form-field class="w-100">
178+
<mat-form-field class="w-100" subscriptSizing="dynamic">
179179
<textarea
180180
[cdkTextareaAutosize]="true"
181+
[formControl]="htmlInput"
181182
[(ngModel)]="applicationFormItem.i18n[lang].label"
182183
matInput></textarea>
184+
<mat-error
185+
*ngIf="htmlInput.invalid && (htmlInput.dirty || htmlInput.touched)"
186+
>{{htmlInput.errors.invalidHtmlContent}}</mat-error
187+
>
183188
</mat-form-field>
184189
</app-edit-application-form-item-line>
185190
</div>
@@ -275,18 +280,23 @@ <h1 mat-dialog-title>
275280
applicationFormItem.federationAttribute !== '' ||
276281
(applicationFormItem.disabled !== 'ALWAYS' && applicationFormItem.hidden !== 'ALWAYS')"
277282
matTooltip="{{'DIALOGS.APPLICATION_FORM_EDIT_ITEM.SUBMIT_BUTTON_DISABLED_TOOLTIP' | translate}}">
278-
<button
279-
mat-flat-button
280-
class="ms-2 mt-auto"
281-
color="accent"
282-
data-cy="edit-form-item-button-dialog"
283-
[disabled]="loading || (applicationFormItem.required &&
284-
applicationFormItem.perunSourceAttribute === '' &&
285-
applicationFormItem.federationAttribute === '' &&
286-
(applicationFormItem.disabled === 'ALWAYS' || applicationFormItem.hidden === 'ALWAYS'))"
287-
(click)="submit()">
288-
{{'DIALOGS.APPLICATION_FORM_EDIT_ITEM.SUBMIT_BUTTON' | translate}}
289-
</button>
283+
<div
284+
[matTooltipDisabled]="!htmlInput.invalid"
285+
[matTooltipPosition]="'above'"
286+
matTooltip="{{'DIALOGS.APPLICATION_FORM_EDIT_ITEM.HTML_INVALID_TAGS' | translate}}">
287+
<button
288+
mat-flat-button
289+
class="ms-2 mt-auto"
290+
color="accent"
291+
data-cy="edit-form-item-button-dialog"
292+
[disabled]="htmlInput.invalid || loading || (applicationFormItem.required &&
293+
applicationFormItem.perunSourceAttribute === '' &&
294+
applicationFormItem.federationAttribute === '' &&
295+
(applicationFormItem.disabled === 'ALWAYS' || applicationFormItem.hidden === 'ALWAYS'))"
296+
(click)="submit()">
297+
{{'DIALOGS.APPLICATION_FORM_EDIT_ITEM.SUBMIT_BUTTON' | translate}}
298+
</button>
299+
</div>
290300
</div>
291301
</div>
292302
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
::ng-deep .mat-mdc-form-field-error-wrapper {
2+
position: relative !important;
3+
word-break: break-word;
4+
}

apps/admin-gui/src/app/shared/components/dialogs/edit-application-form-item-dialog/edit-application-form-item-dialog.component.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import { createNewApplicationFormItem } from '@perun-web-apps/perun/utils';
1313
import DisabledEnum = ApplicationFormItem.DisabledEnum;
1414
import HiddenEnum = ApplicationFormItem.HiddenEnum;
1515
import { ItemType, NO_FORM_ITEM, SelectionItem } from '@perun-web-apps/perun/components';
16-
import { StoreService } from '@perun-web-apps/perun/services';
16+
import { HtmlEscapeService, StoreService } from '@perun-web-apps/perun/services';
17+
import { FormControl } from '@angular/forms';
1718

1819
export interface EditApplicationFormItemDialogComponentData {
1920
theme: string;
@@ -40,6 +41,7 @@ export class EditApplicationFormItemDialogComponent implements OnInit {
4041
hiddenValues: HiddenEnum[] = ['NEVER', 'ALWAYS', 'IF_EMPTY', 'IF_PREFILLED'];
4142
disabledValues: DisabledEnum[] = ['NEVER', 'ALWAYS', 'IF_EMPTY', 'IF_PREFILLED'];
4243
possibleDependencyItems: ApplicationFormItem[] = [];
44+
htmlInput = new FormControl('', [this.escapeService.htmlContentValidator()]);
4345

4446
typesWithUpdatable: Type[] = [
4547
'VALIDATED_EMAIL',
@@ -86,7 +88,8 @@ export class EditApplicationFormItemDialogComponent implements OnInit {
8688
private attributesManager: AttributesManagerService,
8789
private translateService: TranslateService,
8890
private store: StoreService,
89-
private cd: ChangeDetectorRef
91+
private cd: ChangeDetectorRef,
92+
private escapeService: HtmlEscapeService
9093
) {}
9194

9295
ngOnInit(): void {
@@ -131,6 +134,9 @@ export class EditApplicationFormItemDialogComponent implements OnInit {
131134
this.applicationFormItem.perunSourceAttribute = '';
132135
}
133136
this.getOptions();
137+
138+
this.htmlInput.markAsTouched();
139+
this.htmlInput.setValue(this.applicationFormItem.i18n['en'].label);
134140
}
135141

136142
cancel(): void {
@@ -142,6 +148,20 @@ export class EditApplicationFormItemDialogComponent implements OnInit {
142148
this.hiddenDependencyItem === NO_FORM_ITEM ? null : this.hiddenDependencyItem.id;
143149
this.applicationFormItem.disabledDependencyItemId =
144150
this.disabledDependencyItem === NO_FORM_ITEM ? null : this.disabledDependencyItem.id;
151+
152+
// Escape forbidden strings
153+
if (
154+
this.applicationFormItem.type === 'HTML_COMMENT' ||
155+
this.applicationFormItem.type === 'HEADING'
156+
) {
157+
for (const lang in this.applicationFormItem.i18n) {
158+
const o = this.applicationFormItem.i18n[lang];
159+
this.applicationFormItem.i18n[lang].label = this.escapeService.escapeDangerousHtml(
160+
o.label
161+
).escapedHtml;
162+
}
163+
}
164+
145165
this.updateOptions();
146166
this.copy(this.applicationFormItem, this.data.applicationFormItem);
147167
this.dialogRef.close(true);

apps/admin-gui/src/app/shared/components/dialogs/edit-email-footer-dialog/edit-email-footer-dialog.component.html

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,23 @@ <h1 mat-dialog-title>{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.TITLE' | translate}}</
1515
{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.FORMAT_' + format | uppercase | translate}}
1616
</span>
1717
</ng-template>
18-
<mat-form-field class="w-100">
18+
<mat-form-field class="w-100" subscriptSizing="dynamic">
1919
<textarea *ngIf="format === 'plain_text'" [(ngModel)]="mailFooter" matInput rows="5">
2020
</textarea>
21-
<textarea *ngIf="format === 'html'" [(ngModel)]="htmlMailFooter" matInput rows="5">
21+
<textarea *ngIf="format === 'html'" [formControl]="htmlInput" matInput rows="5">
2222
</textarea>
23+
<mat-error *ngIf="htmlInput.invalid && format==='html'">
24+
{{htmlInput.errors.invalidHtmlContent}}
25+
</mat-error>
2326
</mat-form-field>
27+
<div class="mt-2 font-italic text-muted" *ngIf="format === 'plain_text'">
28+
{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.DESCRIPTION' | translate}}
29+
</div>
30+
<div class="mt-2 font-italic text-muted" *ngIf="format === 'html'">
31+
{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.DESCRIPTION_HTML' | translate}}
32+
</div>
2433
</mat-tab>
2534
</mat-tab-group>
26-
27-
<div class="mt-2 font-italic text-muted">
28-
{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.DESCRIPTION' | translate}}
29-
</div>
3035
</div>
3136

3237
<div mat-dialog-actions>
@@ -37,14 +42,19 @@ <h1 mat-dialog-title>{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.TITLE' | translate}}</
3742
[matTooltipDisabled]="plainEdithAuth"
3843
[matTooltipPosition]="'above'"
3944
matTooltip="{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.HINT' | translate}}">
40-
<button
41-
(click)="submit()"
42-
[disabled]="loading || !plainEdithAuth"
43-
class="ms-2"
44-
color="accent"
45-
mat-flat-button>
46-
{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.SUBMIT_BUTTON' | translate}}
47-
</button>
45+
<div
46+
[matTooltipDisabled]="!htmlInput.invalid"
47+
[matTooltipPosition]="'above'"
48+
matTooltip="{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.INVALID_TAGS' | translate}}">
49+
<button
50+
(click)="submit()"
51+
[disabled]="loading || !plainEdithAuth || htmlInput.invalid"
52+
class="ms-2"
53+
color="accent"
54+
mat-flat-button>
55+
{{'DIALOGS.NOTIFICATIONS_EDIT_FOOTER.SUBMIT_BUTTON' | translate}}
56+
</button>
57+
</div>
4858
</div>
4959
</div>
5060
</div>

0 commit comments

Comments
 (0)