From 11dd39d8912e33bf7bde25b3c70ca088b267c223 Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:39:19 +0100 Subject: [PATCH] LINDAT/UFAL/Fix dspace-customers 1083 R68: sanitize RefBox citation HTML (1662) The citation text came from the server and was rendered with bypassSecurityTrustHtml, so markup in item metadata could run scripts. It now goes through Angular's sanitizer, which keeps the and formatting, strips scripts and event handlers, and makes javascript: links inert. Co-authored-by: Claude Opus 5.5 (cherry picked from commit a5298dd8ea60f9d05a42d7275a769f8613cbeb80) --- .../clarin-ref-citation.component.spec.ts | 56 +++++++++++++++++++ .../clarin-ref-citation.component.ts | 10 ++-- 2 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.spec.ts diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.spec.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.spec.ts new file mode 100644 index 00000000000..9be03b198f6 --- /dev/null +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { of as observableOf } from 'rxjs'; +import { ClarinRefCitationComponent } from './clarin-ref-citation.component'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { RequestService } from '../../core/data/request.service'; +import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { Item } from '../../core/shared/item.model'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; + +describe('ClarinRefCitationComponent', () => { + const handleUrl = 'https://hdl.handle.net/123456789/1'; + const displayText = 'Doe, 2024, Title, ' + + 'bad link, ' + handleUrl + '.' + + ''; + + let fixture: ComponentFixture; + let citation: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ClarinRefCitationComponent], + imports: [TranslateModule.forRoot(), NgbTooltipModule], + providers: [ + { provide: ConfigurationDataService, useValue: {} }, + { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['generateRequestId', 'send']) }, + { provide: HALEndpointService, useValue: { getRootHref: () => 'https://rest.test/server/api' } }, + { + provide: RemoteDataBuildService, useValue: jasmine.createSpyObj('rdbService', { + buildFromRequestUUID: observableOf(createSuccessfulRemoteDataObject({ displayText })), + }) + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ClarinRefCitationComponent); + fixture.componentInstance.item = Object.assign(new Item(), { handle: '123456789/1', metadata: {} }); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + citation = fixture.nativeElement.querySelector('.clarin-ref-box-text'); + }); + + it('should not render scripts or event handlers from the display text', () => { + expect(citation.querySelector('script')).toBeNull(); + expect(citation.querySelector('[onerror]')).toBeNull(); + expect(citation.querySelector('a[href^="javascript:"]')).toBeNull(); + }); + + it('should keep the citation formatting', () => { + expect(citation.querySelector('i').textContent).toBe('Title'); + expect(citation.querySelector('a[href="' + handleUrl + '"]').textContent).toBe(handleUrl); + }); +}); diff --git a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts index 94980315738..b014bf8bedb 100644 --- a/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts +++ b/src/app/item-page/clarin-ref-citation/clarin-ref-citation.component.ts @@ -10,7 +10,6 @@ import { RequestService } from '../../core/data/request.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { BehaviorSubject } from 'rxjs'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; /** @@ -42,9 +41,9 @@ export class ClarinRefCitationComponent implements OnInit { /** * The content of the reference box, which will be displayed in the tooltip. - * This content is fetched from the RefBox Controller. + * This content is fetched from the RefBox Controller and sanitized by Angular when rendered. */ - refboxContent: BehaviorSubject = new BehaviorSubject(null); + refboxContent: BehaviorSubject = new BehaviorSubject(null); /** * The raw content of the reference box, which is fetched from the RefBox Controller. @@ -62,8 +61,7 @@ export class ClarinRefCitationComponent implements OnInit { private modalService: NgbModal, private requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected halService: HALEndpointService, - private sanitizer: DomSanitizer) { + protected halService: HALEndpointService) { // Configure the tooltip to show on click - `Copied` message config.triggers = 'click'; } @@ -72,7 +70,7 @@ export class ClarinRefCitationComponent implements OnInit { void this.fetchRefBoxContent() .then((content) => { this.refboxCopyContent = content; // Store raw HTML - this.refboxContent.next(this.sanitizer.bypassSecurityTrustHtml(content)); + this.refboxContent.next(content); }).catch((error) => { console.error('Failed to fetch refbox content:', error); this.refboxCopyContent = this.EMPTY_CONTENT;