Skip to content

Commit 8e9d039

Browse files
authored
PD-5100 (#2759)
1 parent 7eec44a commit 8e9d039

7 files changed

Lines changed: 79 additions & 8 deletions

File tree

projects/orcid-registry-ui/src/lib/components/permission-notifications/permission-notifications.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Output,
77
} from '@angular/core'
88
import { DomSanitizer } from '@angular/platform-browser'
9+
import { sanitizeHtmlForTrustedBypass } from '../../utils/sanitize-html-for-trusted-bypass'
910
import {
1011
NgClass,
1112
NgFor,
@@ -80,7 +81,8 @@ export class PermissionNotificationsComponent {
8081
constructor(private _sanitizer: DomSanitizer) {}
8182

8283
getTrustedHtml(text: string) {
83-
return this._sanitizer.bypassSecurityTrustHtml(text)
84+
const safe = sanitizeHtmlForTrustedBypass(text ?? '')
85+
return this._sanitizer.bypassSecurityTrustHtml(safe)
8486
}
8587

8688
get visibleNotifications(): RegistryPermissionNotification[] {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Sanitizes HTML by removing script and style tags (and their content) so the
3+
* result is safe to pass to DomSanitizer.bypassSecurityTrustHtml().
4+
*/
5+
export function sanitizeHtmlForTrustedBypass(html: string): string {
6+
if (typeof html !== 'string') {
7+
return ''
8+
}
9+
return html
10+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
11+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '')
12+
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { Pipe, PipeTransform } from '@angular/core'
22
import { DomSanitizer } from '@angular/platform-browser'
3+
import { sanitizeHtmlForTrustedBypass } from '../../utils/sanitize-html-for-trusted-bypass'
34

45
@Pipe({
56
name: 'safeHtml',
67
standalone: false,
78
})
89
export class SafeHtmlPipe implements PipeTransform {
910
constructor(private sanitized: DomSanitizer) {}
10-
transform(value) {
11-
return this.sanitized.bypassSecurityTrustHtml(value)
11+
transform(value: string | null | undefined) {
12+
if (value == null) return value
13+
const safe = sanitizeHtmlForTrustedBypass(String(value))
14+
return this.sanitized.bypassSecurityTrustHtml(safe)
1215
}
1316
}

src/app/shared/pipes/search-term-highlight/search-term-highlight.pipe.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { DomSanitizer } from '@angular/platform-browser'
22
import { SearchTermHighlightPipe } from './search-term-highlight.pipe'
3-
import { SecurityContext } from '@angular/core'
43

54
describe('HighlightPipe', () => {
65
let sanitizer: DomSanitizer
76
let pipe: SearchTermHighlightPipe
87

98
beforeEach(() => {
109
sanitizer = {
11-
sanitize: (ctx: SecurityContext, value: any) => value,
1210
bypassSecurityTrustHtml: (value: string) => value,
1311
} as any
1412

@@ -63,6 +61,14 @@ describe('HighlightPipe', () => {
6361
expect(pipe.transform(value, searchTerm)).toBe(value)
6462
})
6563

64+
it('should strip script tags via sanitizer so scripts are not executed (XSS-safe)', () => {
65+
const value = 'Hello <script>alert(1)</script> world'
66+
const searchTerm = 'world'
67+
const result = pipe.transform(value, searchTerm) as string
68+
expect(result).not.toContain('<script>')
69+
expect(result).toContain('<span class="highlight">world</span>')
70+
})
71+
6672
it('should handle special regex characters in the search term by escaping them', () => {
6773
const value = 'example with a.dot'
6874
const searchTerm = 'a.dot'
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Pipe, PipeTransform } from '@angular/core'
22
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3+
import { sanitizeHtmlForTrustedBypass } from '../../utils/sanitize-html-for-trusted-bypass'
34

45
@Pipe({
56
name: 'searchTermHighlight',
@@ -8,15 +9,20 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
89
export class SearchTermHighlightPipe implements PipeTransform {
910
constructor(private sanitizer: DomSanitizer) {}
1011

11-
transform(value: string, searchTerm: string): SafeHtml {
12+
transform(
13+
value: string | null | undefined,
14+
searchTerm: string | null | undefined
15+
): SafeHtml | string | null | undefined {
1216
if (!value || !searchTerm) {
1317
return value
1418
}
15-
const regex = new RegExp(searchTerm, 'gi')
19+
const escapedSearch = searchTerm.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
20+
const regex = new RegExp(escapedSearch, 'gi')
1621
const replacedValue = value.replace(
1722
regex,
1823
(match) => `<span class="highlight">${match}</span>`
1924
)
20-
return this.sanitizer.bypassSecurityTrustHtml(replacedValue)
25+
const sanitized = sanitizeHtmlForTrustedBypass(replacedValue)
26+
return this.sanitizer.bypassSecurityTrustHtml(sanitized)
2127
}
2228
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { sanitizeHtmlForTrustedBypass } from './sanitize-html-for-trusted-bypass'
2+
3+
describe('sanitizeHtmlForTrustedBypass', () => {
4+
it('strips script tags and their content', () => {
5+
const html = 'Hello <script>alert(1)</script> world'
6+
expect(sanitizeHtmlForTrustedBypass(html)).toBe('Hello world')
7+
})
8+
9+
it('strips script tags with attributes', () => {
10+
const html = 'Foo <script type="text/javascript">evil()</script> bar'
11+
expect(sanitizeHtmlForTrustedBypass(html)).toBe('Foo bar')
12+
})
13+
14+
it('strips style tags and their content', () => {
15+
const html = 'Text <style>.x { color: red }</style> more'
16+
expect(sanitizeHtmlForTrustedBypass(html)).toBe('Text more')
17+
})
18+
19+
it('leaves safe markup intact', () => {
20+
const html = '<span class="highlight">safe</span>'
21+
expect(sanitizeHtmlForTrustedBypass(html)).toBe(html)
22+
})
23+
24+
it('returns empty string for non-string input', () => {
25+
expect(sanitizeHtmlForTrustedBypass(null as any)).toBe('')
26+
expect(sanitizeHtmlForTrustedBypass(undefined as any)).toBe('')
27+
})
28+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Sanitizes HTML by removing script and style tags (and their content) so the
3+
* result is safe to pass to DomSanitizer.bypassSecurityTrustHtml().
4+
* Use this whenever you need to render HTML that may contain user/content while
5+
* allowing only safe markup (e.g. no scripts).
6+
*/
7+
export function sanitizeHtmlForTrustedBypass(html: string): string {
8+
if (typeof html !== 'string') {
9+
return ''
10+
}
11+
return html
12+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
13+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '')
14+
}

0 commit comments

Comments
 (0)