Skip to content

Commit dda0033

Browse files
committed
Merge pull request from GHSA-4m77-cmpx-vjc4
* Fix sanitization and escaping for MD headings in ToC * Add a unit test for `getHeadingId` * Make `sanitizer` argument optional in getHeadingId, integrity update (cherry picked from commit e1b3aab)
1 parent 0708330 commit dda0033

File tree

11 files changed

+85
-26
lines changed

11 files changed

+85
-26
lines changed

packages/markdownviewer-extension/src/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
JupyterFrontEnd,
1111
JupyterFrontEndPlugin
1212
} from '@jupyterlab/application';
13-
import { WidgetTracker } from '@jupyterlab/apputils';
13+
import { ISanitizer, WidgetTracker } from '@jupyterlab/apputils';
1414
import { PathExt } from '@jupyterlab/coreutils';
1515
import {
1616
IMarkdownViewerTracker,
@@ -20,6 +20,7 @@ import {
2020
MarkdownViewerTableOfContentsFactory
2121
} from '@jupyterlab/markdownviewer';
2222
import {
23+
IRenderMime,
2324
IRenderMimeRegistry,
2425
markdownRendererFactory
2526
} from '@jupyterlab/rendermime';
@@ -49,7 +50,12 @@ const plugin: JupyterFrontEndPlugin<IMarkdownViewerTracker> = {
4950
description: 'Adds markdown file viewer and provides its tracker.',
5051
provides: IMarkdownViewerTracker,
5152
requires: [IRenderMimeRegistry, ITranslator],
52-
optional: [ILayoutRestorer, ISettingRegistry, ITableOfContentsRegistry],
53+
optional: [
54+
ILayoutRestorer,
55+
ISettingRegistry,
56+
ITableOfContentsRegistry,
57+
ISanitizer
58+
],
5359
autoStart: true
5460
};
5561

@@ -62,7 +68,8 @@ function activate(
6268
translator: ITranslator,
6369
restorer: ILayoutRestorer | null,
6470
settingRegistry: ISettingRegistry | null,
65-
tocRegistry: ITableOfContentsRegistry | null
71+
tocRegistry: ITableOfContentsRegistry | null,
72+
sanitizer: IRenderMime.ISanitizer | null
6673
): IMarkdownViewerTracker {
6774
const trans = translator.load('jupyterlab');
6875
const { commands, docRegistry } = app;
@@ -182,7 +189,8 @@ function activate(
182189
tocRegistry.add(
183190
new MarkdownViewerTableOfContentsFactory(
184191
tracker,
185-
rendermime.markdownParser
192+
rendermime.markdownParser,
193+
sanitizer ?? rendermime.sanitizer
186194
)
187195
);
188196
}

packages/markdownviewer/src/toc.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import { IWidgetTracker } from '@jupyterlab/apputils';
5-
import { IMarkdownParser } from '@jupyterlab/rendermime';
5+
import { IMarkdownParser, IRenderMime } from '@jupyterlab/rendermime';
66
import {
77
TableOfContents,
88
TableOfContentsFactory,
@@ -96,7 +96,8 @@ export class MarkdownViewerTableOfContentsFactory extends TableOfContentsFactory
9696
*/
9797
constructor(
9898
tracker: IWidgetTracker<MarkdownDocument>,
99-
protected parser: IMarkdownParser | null
99+
protected parser: IMarkdownParser | null,
100+
protected sanitizer: IRenderMime.ISanitizer
100101
) {
101102
super(tracker);
102103
}
@@ -165,13 +166,14 @@ export class MarkdownViewerTableOfContentsFactory extends TableOfContentsFactory
165166
const elementId = await TableOfContentsUtils.Markdown.getHeadingId(
166167
this.parser!,
167168
heading.raw,
168-
heading.level
169+
heading.level,
170+
this.sanitizer
169171
);
170172

171173
if (!elementId) {
172174
return;
173175
}
174-
const selector = `h${heading.level}[id="${elementId}"]`;
176+
const selector = `h${heading.level}[id="${CSS.escape(elementId)}"]`;
175177

176178
headingToElement.set(
177179
heading,

packages/notebook/src/toc.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -588,10 +588,14 @@ export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {
588588

589589
const findHeadingElement = (cell: Cell): void => {
590590
model.getCellHeadings(cell).forEach(async heading => {
591-
const elementId = await getIdForHeading(heading, this.parser!);
591+
const elementId = await getIdForHeading(
592+
heading,
593+
this.parser!,
594+
this.sanitizer
595+
);
592596

593597
const selector = elementId
594-
? `h${heading.level}[id="${elementId}"]`
598+
? `h${heading.level}[id="${CSS.escape(elementId)}"]`
595599
: `h${heading.level}`;
596600

597601
if (heading.outputIndex !== undefined) {
@@ -710,15 +714,17 @@ export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {
710714
*/
711715
export async function getIdForHeading(
712716
heading: INotebookHeading,
713-
parser: IRenderMime.IMarkdownParser
717+
parser: IRenderMime.IMarkdownParser,
718+
sanitizer: IRenderMime.ISanitizer
714719
) {
715720
let elementId: string | null = null;
716721
if (heading.type === Cell.HeadingType.Markdown) {
717722
elementId = await TableOfContentsUtils.Markdown.getHeadingId(
718723
parser,
719724
// Type from TableOfContentsUtils.Markdown.IMarkdownHeading
720725
(heading as any).raw,
721-
heading.level
726+
heading.level,
727+
sanitizer
722728
);
723729
} else if (heading.type === Cell.HeadingType.HTML) {
724730
// Type from TableOfContentsUtils.IHTMLHeading

packages/notebook/src/widget.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,14 +2191,15 @@ export class Notebook extends StaticNotebook {
21912191
id = await TableOfContentsUtils.Markdown.getHeadingId(
21922192
this.rendermime.markdownParser!,
21932193
mdHeading.raw,
2194-
mdHeading.level
2194+
mdHeading.level,
2195+
this.rendermime.sanitizer
21952196
);
21962197
}
21972198
break;
21982199
}
21992200
if (id === queryId) {
22002201
const element = this.node.querySelector(
2201-
`h${heading.level}[id="${id}"]`
2202+
`h${heading.level}[id="${CSS.escape(id)}"]`
22022203
) as HTMLElement;
22032204

22042205
return {

packages/rendermime-interfaces/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,10 +486,10 @@ export namespace IRenderMime {
486486
*/
487487
export interface IMarkdownParser {
488488
/**
489-
* Render a markdown source.
489+
* Render a markdown source into unsanitized HTML.
490490
*
491491
* @param source - The string to render.
492-
* @returns - A promise of the string.
492+
* @returns - A promise of the string containing HTML which may require sanitization.
493493
*/
494494
render(source: string): Promise<string>;
495495
}

packages/toc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@jupyterlab/docregistry": "^4.0.10",
4747
"@jupyterlab/observables": "^5.0.10",
4848
"@jupyterlab/rendermime": "^4.0.10",
49+
"@jupyterlab/rendermime-interfaces": "^3.8.10",
4950
"@jupyterlab/translation": "^4.0.10",
5051
"@jupyterlab/ui-components": "^4.0.10",
5152
"@lumino/coreutils": "^2.1.2",

packages/toc/src/utils/markdown.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4+
import { Sanitizer } from '@jupyterlab/apputils';
45
import { IMarkdownParser, renderMarkdown } from '@jupyterlab/rendermime';
6+
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
57
import { TableOfContents } from '../tokens';
68

79
/**
@@ -24,27 +26,35 @@ export interface IMarkdownHeading extends TableOfContents.IHeading {
2426
*
2527
* @param raw Raw markdown heading
2628
* @param level Heading level
29+
* @param sanitizer HTML sanitizer
2730
*/
2831
export async function getHeadingId(
29-
parser: IMarkdownParser,
32+
markdownParser: IMarkdownParser,
3033
raw: string,
31-
level: number
34+
level: number,
35+
sanitizer?: IRenderMime.ISanitizer
3236
): Promise<string | null> {
3337
try {
34-
const innerHTML = await parser.render(raw);
38+
const host = document.createElement('div');
3539

36-
if (!innerHTML) {
37-
return null;
38-
}
40+
await renderMarkdown({
41+
markdownParser,
42+
host,
43+
source: raw,
44+
trusted: false,
45+
sanitizer: sanitizer ?? new Sanitizer(),
46+
shouldTypeset: false,
47+
resolver: null,
48+
linkHandler: null,
49+
latexTypesetter: null
50+
});
3951

40-
const container = document.createElement('div');
41-
container.innerHTML = innerHTML;
42-
const header = container.querySelector(`h${level}`);
52+
const header = host.querySelector(`h${level}`);
4353
if (!header) {
4454
return null;
4555
}
4656

47-
return renderMarkdown.createHeaderId(header);
57+
return header.id;
4858
} catch (reason) {
4959
console.error('Failed to parse a heading.', reason);
5060
}

packages/toc/test/markdown.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,33 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import { TableOfContentsUtils } from '@jupyterlab/toc';
5+
import { Sanitizer } from '@jupyterlab/apputils';
6+
import { createMarkdownParser } from '@jupyterlab/markedparser-extension';
7+
import { IMarkdownParser } from '@jupyterlab/rendermime';
8+
import {
9+
EditorLanguageRegistry,
10+
IEditorLanguageRegistry
11+
} from '@jupyterlab/codemirror';
512

613
describe('TableOfContentsUtils', () => {
714
describe('Markdown', () => {
15+
describe('#getHeadingId', () => {
16+
const languages: IEditorLanguageRegistry = new EditorLanguageRegistry();
17+
const parser: IMarkdownParser = createMarkdownParser(languages);
18+
const sanitizer = new Sanitizer();
19+
it.each<[string, string]>([
20+
['# Title', 'Title'],
21+
[`# test'"></title><img>test {#'"><img>}`, `test'\">test-{#'\">}`]
22+
])('should derive ID from markdown', async (markdown, expectedId) => {
23+
const headingId = await TableOfContentsUtils.Markdown.getHeadingId(
24+
parser,
25+
markdown,
26+
1,
27+
sanitizer
28+
);
29+
expect(headingId).toEqual(expectedId);
30+
});
31+
});
832
describe('#getHeadings', () => {
933
it.each<[string, TableOfContentsUtils.Markdown.IMarkdownHeading[]]>([
1034
[

packages/toc/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
},
2727
{
2828
"path": "../ui-components"
29+
},
30+
{
31+
"path": "../rendermime-interfaces"
2932
}
3033
]
3134
}

packages/toc/tsconfig.test.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
{
2727
"path": "../ui-components"
2828
},
29+
{
30+
"path": "../rendermime-interfaces"
31+
},
2932
{
3033
"path": "."
3134
},

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy