Security: remove safe html pipe

This commit is contained in:
shamoon
2025-12-18 06:31:25 -08:00
parent 84c59f45da
commit bf38ae98f1
33 changed files with 44 additions and 107 deletions

View File

@@ -28,7 +28,6 @@ import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
@@ -129,7 +128,6 @@ describe('SettingsComponent', () => {
ConfirmDialogComponent,
CheckComponent,
ColorComponent,
SafeHtmlPipe,
SelectComponent,
TextComponent,
NumberComponent,

View File

@@ -11,7 +11,6 @@ import {
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
@@ -53,7 +52,6 @@ describe('TrashComponent', () => {
TrashComponent,
PageHeaderComponent,
ConfirmDialogComponent,
SafeHtmlPipe,
],
}).compileComponents()

View File

@@ -8,7 +8,7 @@
<p><b>{{messageBold}}</b></p>
}
@if (message) {
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
<p class="mb-0" [innerHTML]="message"></p>
}
</div>
<div class="modal-footer">

View File

@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject } from 'rxjs'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ConfirmDialogComponent } from './confirm-dialog.component'
describe('ConfirmDialogComponent', () => {
@@ -11,8 +10,8 @@ describe('ConfirmDialogComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [NgbActiveModal, SafeHtmlPipe],
imports: [ConfirmDialogComponent, SafeHtmlPipe],
providers: [NgbActiveModal],
imports: [ConfirmDialogComponent],
}).compileComponents()
modal = TestBed.inject(NgbActiveModal)

View File

@@ -2,14 +2,13 @@ import { DecimalPipe } from '@angular/common'
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject } from 'rxjs'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss'],
imports: [DecimalPipe, SafeHtmlPipe],
imports: [DecimalPipe],
})
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
activeModal = inject(NgbActiveModal)

View File

@@ -28,10 +28,10 @@
<div class="modal-footer flex-nowrap">
<div class="col">
@if (message) {
<p [innerHTML]="message | safeHtml"></p>
<p>{{message}}</p>
}
@if (messageBold) {
<p class="mb-0 small"><b [innerHTML]="messageBold | safeHtml"></b></p>
<p class="mb-0 small"><b>{{messageBold}}</b></p>
}
</div>
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">

View File

@@ -3,7 +3,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
describe('RotateConfirmDialogComponent', () => {
@@ -15,11 +14,9 @@ describe('RotateConfirmDialogComponent', () => {
imports: [
NgxBootstrapIconsModule.pick(allIcons),
RotateConfirmDialogComponent,
SafeHtmlPipe,
],
providers: [
NgbActiveModal,
SafeHtmlPipe,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],

View File

@@ -1,7 +1,6 @@
import { NgStyle } from '@angular/common'
import { Component, inject } from '@angular/core'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@@ -9,7 +8,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
selector: 'pngx-rotate-confirm-dialog',
templateUrl: './rotate-confirm-dialog.component.html',
styleUrl: './rotate-confirm-dialog.component.scss',
imports: [NgStyle, NgxBootstrapIconsModule, SafeHtmlPipe],
imports: [NgStyle, NgxBootstrapIconsModule],
})
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
documentService = inject(DocumentService)

View File

@@ -10,7 +10,6 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { CustomFieldDataType } from 'src/app/data/custom-field'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { SettingsService } from 'src/app/services/settings.service'
import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component'
@@ -35,7 +34,6 @@ describe('CustomFieldEditDialogComponent', () => {
IfOwnerDirective,
SelectComponent,
TextComponent,
SafeHtmlPipe,
],
providers: [
NgbActiveModal,

View File

@@ -11,7 +11,6 @@ import {
} from 'src/app/data/mail-rule'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
@@ -46,7 +45,6 @@ describe('MailRuleEditDialogComponent', () => {
PermissionsFormComponent,
NumberComponent,
TagsComponent,
SafeHtmlPipe,
CheckComponent,
SwitchComponent,
],

View File

@@ -30,7 +30,6 @@ import {
} from 'src/app/data/workflow-trigger'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
@@ -105,7 +104,6 @@ describe('WorkflowEditDialogComponent', () => {
TagsComponent,
PermissionsUserComponent,
PermissionsGroupComponent,
SafeHtmlPipe,
ConfirmButtonComponent,
],
providers: [

View File

@@ -19,7 +19,7 @@
</div>
}
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<small class="form-text text-muted" [innerHTML]="hint"></small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}

View File

@@ -24,7 +24,7 @@
}
<input #inputField type="hidden" class="form-control small" [(ngModel)]="value" [disabled]="true">
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<small class="form-text text-muted" [innerHTML]="hint"></small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}

View File

@@ -12,6 +12,6 @@
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<small class="form-text text-muted" [innerHTML]="hint"></small>
}
</div>

View File

@@ -13,7 +13,7 @@
<div class="position-relative" [class.col-md-9]="horizontal">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete" [placeholder]="placeholder">
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<small class="form-text text-muted" [innerHTML]="hint"></small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}

View File

@@ -5,7 +5,6 @@ import {
ReactiveFormsModule,
} from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { AbstractInputComponent } from '../abstract-input'
@Component({
@@ -19,12 +18,7 @@ import { AbstractInputComponent } from '../abstract-input'
selector: 'pngx-input-text',
templateUrl: './text.component.html',
styleUrls: ['./text.component.scss'],
imports: [
FormsModule,
ReactiveFormsModule,
SafeHtmlPipe,
NgxBootstrapIconsModule,
],
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
})
export class TextComponent extends AbstractInputComponent<string> {
@Input()

View File

@@ -23,7 +23,7 @@
rows="4">
</textarea>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<small class="form-text text-muted" [innerHTML]="hint"></small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}

View File

@@ -5,7 +5,6 @@ import {
ReactiveFormsModule,
} from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { AbstractInputComponent } from '../abstract-input'
@Component({
@@ -19,12 +18,7 @@ import { AbstractInputComponent } from '../abstract-input'
selector: 'pngx-input-textarea',
templateUrl: './textarea.component.html',
styleUrls: ['./textarea.component.scss'],
imports: [
FormsModule,
ReactiveFormsModule,
SafeHtmlPipe,
NgxBootstrapIconsModule,
],
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
})
export class TextAreaComponent extends AbstractInputComponent<string> {
@Input()

View File

@@ -19,7 +19,7 @@
</div>
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
<small class="form-text text-muted" [innerHTML]="hint"></small>
}
</div>
</div>

View File

@@ -5,7 +5,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { of } from 'rxjs'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { UserService } from 'src/app/services/rest/user.service'
import { PermissionsFormComponent } from '../input/permissions/permissions-form/permissions-form.component'
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
@@ -41,7 +40,6 @@ describe('PermissionsDialogComponent', () => {
ReactiveFormsModule,
NgbModule,
PermissionsDialogComponent,
SafeHtmlPipe,
SelectComponent,
SwitchComponent,
PermissionsFormComponent,

View File

@@ -110,7 +110,9 @@
<div class="visually-hidden" i18n>Loading...</div>
} @else if (totpSettings) {
<figure class="figure">
<div class="bg-white d-inline-block" [innerHTML]="totpSettings.qr_svg | safeHtml"></div>
@if (qrSvgDataUrl) {
<img class="bg-white d-inline-block" [src]="qrSvgDataUrl" alt="Authenticator QR code">
}
<figcaption class="figure-caption text-end mt-2" i18n>Scan the QR code with your authenticator app and then enter the code below</figcaption>
</figure>
<p>

View File

@@ -18,7 +18,6 @@ import {
SocialAccountProvider,
TotpSettings,
} from 'src/app/data/user-profile'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import { setLocationHref } from 'src/app/utils/navigation'
@@ -37,7 +36,6 @@ import { TextComponent } from '../input/text/text.component'
PasswordComponent,
FormsModule,
ReactiveFormsModule,
SafeHtmlPipe,
NgbAccordionModule,
NgbPopoverModule,
NgxBootstrapIconsModule,
@@ -89,6 +87,13 @@ export class ProfileEditDialogComponent
public socialAccounts: SocialAccount[] = []
public socialAccountProviders: SocialAccountProvider[] = []
get qrSvgDataUrl(): string | null {
if (!this.totpSettings?.qr_svg) {
return null
}
return `data:image/svg+xml;utf8,${encodeURIComponent(this.totpSettings.qr_svg)}`
}
ngOnInit(): void {
this.networkActive = true
this.profileService

View File

@@ -36,7 +36,6 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
import { FilterPipe } from 'src/app/pipes/filter.pipe'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { UsernamePipe } from 'src/app/pipes/username.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { PermissionsService } from 'src/app/services/permissions.service'
@@ -103,7 +102,6 @@ describe('DocumentListComponent', () => {
DatePipe,
DocumentTitlePipe,
UsernamePipe,
SafeHtmlPipe,
PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),

View File

@@ -23,7 +23,6 @@ import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
@@ -84,7 +83,6 @@ describe('MailComponent', () => {
CustomDatePipe,
ConfirmDialogComponent,
CheckComponent,
SafeHtmlPipe,
SelectComponent,
TextComponent,
PasswordComponent,

View File

@@ -94,8 +94,14 @@
<td scope="row">{{ getDocumentCount(object) }}</td>
@for (column of extraColumns; track column) {
<td scope="row" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
@if (column.rendersHtml) {
<div [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div>
@if (column.badgeFn) {
<span
class="badge"
[style.color]="column.badgeFn.call(null, object)?.textColor"
[style.backgroundColor]="column.badgeFn.call(null, object)?.backgroundColor"
>
{{ column.badgeFn.call(null, object)?.text }}
</span>
} @else if (column.monospace) {
<span class="font-monospace">{{ column.valueFn.call(null, object) }}</span>
} @else {

View File

@@ -33,7 +33,6 @@ import { Tag } from 'src/app/data/tag'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import {
PermissionAction,
@@ -93,7 +92,6 @@ describe('ManagementListComponent', () => {
SortableDirective,
PageHeaderComponent,
IfPermissionsDirective,
SafeHtmlPipe,
ConfirmDialogComponent,
PermissionsDialogComponent,
],

View File

@@ -48,9 +48,13 @@ export interface ManagementListColumn {
name: string
valueFn: any
valueFn?: any
rendersHtml?: boolean
badgeFn?: (object: any) => {
text: string
textColor?: string
backgroundColor?: string
}
hideOnMobile?: boolean

View File

@@ -9,7 +9,6 @@ import { of } from 'rxjs'
import { StoragePath } from 'src/app/data/storage-path'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { StoragePathListComponent } from './storage-path-list.component'
@@ -30,7 +29,6 @@ describe('StoragePathListComponent', () => {
SortableDirective,
PageHeaderComponent,
IfPermissionsDirective,
SafeHtmlPipe,
],
providers: [
DatePipe,

View File

@@ -10,7 +10,6 @@ import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
import { StoragePath } from 'src/app/data/storage-path'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionType } from 'src/app/services/permissions.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
@@ -26,7 +25,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
PageHeaderComponent,
TitleCasePipe,
IfPermissionsDirective,
SafeHtmlPipe,
FormsModule,
ReactiveFormsModule,
NgClass,

View File

@@ -8,7 +8,6 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { TagService } from 'src/app/services/rest/tag.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { TagListComponent } from './tag-list.component'
@@ -30,7 +29,6 @@ describe('TagListComponent', () => {
SortableDirective,
PageHeaderComponent,
IfPermissionsDirective,
SafeHtmlPipe,
],
providers: [
DatePipe,

View File

@@ -10,7 +10,6 @@ import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
import { Tag } from 'src/app/data/tag'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SortableDirective } from 'src/app/directives/sortable.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionType } from 'src/app/services/permissions.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
@@ -26,7 +25,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
PageHeaderComponent,
TitleCasePipe,
IfPermissionsDirective,
SafeHtmlPipe,
FormsModule,
ReactiveFormsModule,
NgClass,
@@ -49,10 +47,11 @@ export class TagListComponent extends ManagementListComponent<Tag> {
{
key: 'color',
name: $localize`Color`,
rendersHtml: true,
valueFn: (t: Tag) => {
return `<span class="badge" style="color: ${t.text_color}; background-color: ${t.color}">${t.color}</span>`
},
badgeFn: (t: Tag) => ({
text: t.color,
textColor: t.text_color,
backgroundColor: t.color,
}),
},
]
}

View File

@@ -1,24 +0,0 @@
import { TestBed } from '@angular/core/testing'
import { BrowserModule, DomSanitizer } from '@angular/platform-browser'
import { SafeHtmlPipe } from './safehtml.pipe'
describe('SafeHtmlPipe', () => {
let pipe: SafeHtmlPipe
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SafeHtmlPipe],
imports: [BrowserModule],
})
pipe = TestBed.inject(SafeHtmlPipe)
})
it('should bypass security and trust the url', () => {
const html = '<div>some content</div>'
const domSanitizer = TestBed.inject(DomSanitizer)
const sanitizerSpy = jest.spyOn(domSanitizer, 'bypassSecurityTrustHtml')
let safeHtml = pipe.transform(html)
expect(safeHtml).not.toBeNull()
expect(sanitizerSpy).toHaveBeenCalled()
})
})

View File

@@ -1,13 +0,0 @@
import { Pipe, PipeTransform, inject } from '@angular/core'
import { DomSanitizer } from '@angular/platform-browser'
@Pipe({
name: 'safeHtml',
})
export class SafeHtmlPipe implements PipeTransform {
private sanitizer = inject(DomSanitizer)
transform(html) {
return this.sanitizer.bypassSecurityTrustHtml(html)
}
}