Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { SecurityContext } from '@angular/core'
import { TestBed } from '@angular/core/testing'
import { DomSanitizer } from '@angular/platform-browser'
import { PpwSanitizeHtmlPipe } from './sanitize-html.pipe'

describe('PpwSanitizeHtmlPipe', () => {
let pipe: PpwSanitizeHtmlPipe
let sanitizer: DomSanitizer

beforeEach(() => {
TestBed.configureTestingModule({
providers: [PpwSanitizeHtmlPipe]
})

pipe = TestBed.inject(PpwSanitizeHtmlPipe)
sanitizer = TestBed.inject(DomSanitizer)
})

it('sanitizes HTML before trusting it', () => {
const sanitizeSpy = vi.spyOn(sanitizer, 'sanitize')
const bypassSpy = vi.spyOn(sanitizer, 'bypassSecurityTrustHtml')
const html = '<div><script>alert("xss")</script><p>safe</p></div>'

pipe.transform(html)

expect(sanitizeSpy).toHaveBeenCalledWith(SecurityContext.HTML, html)
expect(bypassSpy).toHaveBeenCalledWith('<div><p>safe</p></div>')
})

it('strips unsafe HTML content from the result', () => {
const html = '<section><script>alert("xss")</script><p>safe</p></section>'

const result = pipe.transform(html)

expect(result).toEqual({
changingThisBreaksApplicationSecurity: '<section><p>safe</p></section>'
})
})

it('returns trusted empty HTML for null input', () => {
const bypassSpy = vi.spyOn(sanitizer, 'bypassSecurityTrustHtml')

const result = pipe.transform(null)

expect(bypassSpy).toHaveBeenCalledWith('')
expect(result).toEqual({
changingThisBreaksApplicationSecurity: ''
})
})
})
34 changes: 34 additions & 0 deletions projects/ppwcode/ng-common/src/lib/pipes/sanitize-html.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { inject, Pipe, PipeTransform, SecurityContext } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'

/**
* Sanitizes an HTML string with Angular's built-in HTML sanitizer and returns the
* sanitized result as `SafeHtml`.
*
* The pipe first removes unsafe markup such as scripts and dangerous attributes
* by calling `DomSanitizer.sanitize(SecurityContext.HTML, value)`. It then wraps
* the sanitized output with `bypassSecurityTrustHtml` so Angular can bind the
* result to `[innerHTML]` without sanitizing it a second time.
*
* Use this pipe when the source value may contain user-provided or otherwise
* untrusted HTML and you want to render the remaining safe markup in the view.
* Typical usage is:
*
* `<div [innerHTML]="htmlContent | ppwSanitizeHtml"></div>`
*
* Do not use this pipe to preserve unsafe markup. Any content removed by the
* sanitizer is intentionally discarded before the result is marked as trusted.
*/
@Pipe({
name: 'ppwSanitizeHtml',
standalone: true
})
export class PpwSanitizeHtmlPipe implements PipeTransform {
private readonly sanitizer = inject(DomSanitizer)

/** Returns trusted HTML that has already been sanitized for safe rendering. */
transform(value: string | null): SafeHtml {
const sanitizedValue = this.sanitizer.sanitize(SecurityContext.HTML, value) ?? ''
return this.sanitizer.bypassSecurityTrustHtml(sanitizedValue)
}
}
1 change: 1 addition & 0 deletions projects/ppwcode/ng-common/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './lib/mixins/handle-subscriptions'
export * from './lib/mixins/responsive-observers'
export * from './lib/mixins/track-pending'
export * from './lib/pipes/api-translate.pipe'
export * from './lib/pipes/sanitize-html.pipe'
export * from './lib/rxjs-operators/truthy-first'
export * from './lib/rxjs-operators/truthy-filter'
export * from './lib/storage/local-storage'
Expand Down
Loading