diff --git a/adev/src/app/editor/code-editor/code-mirror-editor.service.ts b/adev/src/app/editor/code-editor/code-mirror-editor.service.ts index 4931f461608..e508deeab05 100644 --- a/adev/src/app/editor/code-editor/code-mirror-editor.service.ts +++ b/adev/src/app/editor/code-editor/code-mirror-editor.service.ts @@ -19,6 +19,8 @@ import {NodeRuntimeSandbox} from '../node-runtime-sandbox.service'; import {TypingsLoader} from '../typings-loader.service'; import {FileAndContentRecord} from '@angular/docs'; +import {DomSanitizer} from '@angular/platform-browser'; +import {NodeRuntimeState} from '../node-runtime-state.service'; import {CODE_EDITOR_EXTENSIONS} from './constants/code-editor-extensions'; import {LANGUAGES} from './constants/code-editor-languages'; import {getAutocompleteExtension} from './extensions/autocomplete'; @@ -26,10 +28,9 @@ import {getDiagnosticsExtension} from './extensions/diagnostics'; import {getTooltipExtension} from './extensions/tooltip'; import {DiagnosticsState} from './services/diagnostics-state.service'; import {TsVfsWorkerActions} from './workers/enums/actions'; +import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider'; import {CodeChangeRequest} from './workers/interfaces/code-change-request'; import {ActionMessage} from './workers/interfaces/message'; -import {NodeRuntimeState} from '../node-runtime-state.service'; -import {TYPESCRIPT_VFS_WORKER_FACTORY} from './workers/factory-provider'; export interface EditorFile { filename: string; @@ -81,6 +82,7 @@ export class CodeMirrorEditor { private readonly typingsLoader = inject(TypingsLoader); private readonly destroyRef = inject(DestroyRef); private readonly diagnosticsState = inject(DiagnosticsState); + private readonly domSanitizer = inject(DomSanitizer); private readonly tsVfsWorkerFactory = inject(TYPESCRIPT_VFS_WORKER_FACTORY); private tsVfsWorker: Worker | null = null; @@ -448,7 +450,12 @@ export class CodeMirrorEditor { this.sendRequestToTsVfs, this.diagnosticsState, ), - getTooltipExtension(this.eventManager$, this.currentFile, this.sendRequestToTsVfs), + getTooltipExtension( + this.eventManager$, + this.currentFile, + this.sendRequestToTsVfs, + this.domSanitizer, + ), ]; } diff --git a/adev/src/app/editor/code-editor/extensions/tooltip.spec.ts b/adev/src/app/editor/code-editor/extensions/tooltip.spec.ts new file mode 100644 index 00000000000..ea7276280aa --- /dev/null +++ b/adev/src/app/editor/code-editor/extensions/tooltip.spec.ts @@ -0,0 +1,41 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {DomSanitizer} from '@angular/platform-browser'; + +import {TestBed} from '@angular/core/testing'; +import {getMarkedHtmlFromString, getTagsHtml} from './tooltip'; + +describe('getMarkedHtmlFromString', () => { + it('sanitizes markdown HTML content before assigning to innerHTML', () => { + const markdownContent = 'hello '; + const domSanitizer = TestBed.inject(DomSanitizer); + + const result = getMarkedHtmlFromString(markdownContent, domSanitizer); + + expect(result.innerHTML.trim()).toBe('

hello

'); + expect(result.innerHTML).not.toContain('onerror'); + }); +}); + +describe('getTagsHtml', () => { + it('sanitizes JSDoc tag content before assigning to innerHTML', () => { + const tags = [ + { + name: 'example', + text: [{text: 'hello '}], + }, + ] as any[]; + const domSanitizer = TestBed.inject(DomSanitizer); + + const result = getTagsHtml(tags, domSanitizer); + + expect(result.innerHTML).toContain('@example'); + expect(result.innerHTML).not.toContain('onerror'); + }); +}); diff --git a/adev/src/app/editor/code-editor/extensions/tooltip.ts b/adev/src/app/editor/code-editor/extensions/tooltip.ts index 2d2fc49ff2d..413528caf9f 100644 --- a/adev/src/app/editor/code-editor/extensions/tooltip.ts +++ b/adev/src/app/editor/code-editor/extensions/tooltip.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Signal} from '@angular/core'; +import {SecurityContext, Signal} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; + import {Tooltip, hoverTooltip} from '@codemirror/view'; import {marked} from 'marked'; import {Subject, filter, take} from 'rxjs'; @@ -23,6 +25,7 @@ export const getTooltipExtension = ( emitter: Subject>, currentFile: Signal, sendRequestToTsVfs: (request: ActionMessage) => void, + domSanitizer: DomSanitizer, ) => { return hoverTooltip( async (_, pos: number): Promise => { @@ -58,9 +61,9 @@ export const getTooltipExtension = ( // use documentation if available as it's more informative than tags if (documentation?.[0]?.text) { - tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text)); + tooltip.appendChild(getMarkedHtmlFromString(documentation[0]?.text, domSanitizer)); } else if (tags?.length) { - tooltip.appendChild(getTagsHtml(tags)); + tooltip.appendChild(getTagsHtml(tags, domSanitizer)); } return { @@ -91,9 +94,13 @@ function forceTooltipScrollTop() { } } -function getMarkedHtmlFromString(content: string): HTMLDivElement { +export function getMarkedHtmlFromString( + content: string, + domSanitizer: DomSanitizer, +): HTMLDivElement { const wrapper = document.createElement('div'); - wrapper.innerHTML = marked(content) as string; + const sanitizedHtml = renderAndSanitizeMarkdownToHtml(content, domSanitizer); + wrapper.innerHTML = sanitizedHtml; return wrapper; } @@ -123,7 +130,7 @@ function getHtmlFromDisplayParts(displayParts: ts.SymbolDisplayPart[]): HTMLDivE return wrapper; } -function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement { +export function getTagsHtml(tags: ts.JSDocTagInfo[], domSanitizer: DomSanitizer): HTMLDivElement { const tagsWrapper = document.createElement('div'); let contentString = ''; @@ -138,7 +145,14 @@ function getTagsHtml(tags: ts.JSDocTagInfo[]): HTMLDivElement { } } - tagsWrapper.innerHTML = marked(contentString) as string; + const sanitizedHtml = renderAndSanitizeMarkdownToHtml(contentString, domSanitizer); + tagsWrapper.innerHTML = sanitizedHtml; return tagsWrapper; } + +function renderAndSanitizeMarkdownToHtml(content: string, domSanitizer: DomSanitizer): string { + const markedHtml = marked(content) as string; + const sanitizedHtml = domSanitizer.sanitize(SecurityContext.HTML, markedHtml) ?? ''; + return sanitizedHtml; +}