CVE-2024-22420 (TypeScript, published 2024-01-19T21:15Z)
Description: value: JupyterLab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook and Architecture. This vulnerability depends on user interaction by opening a malicious Markdown file using JupyterLab preview feature. A malicious user can access any data that the attacked user has access to as well as perform arbitrary requests acting as the attacked user. JupyterLab version 4.0.11 has been patched. Users are advised to upgrade. Users unable to upgrade should disable the table of contents extension.
Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/jupyterlab/jupyterlab
Files changed:
packages/markdownviewer-extension/src/index.ts
packages/markdownviewer/src/toc.ts
packages/notebook/src/toc.ts
packages/notebook/src/widget.ts
packages/rendermime-interfaces/src/index.ts
packages/toc/package.json
packages/toc/src/utils/markdown.ts
packages/toc/test/markdown.spec.ts
packages/toc/tsconfig.json
packages/toc/tsconfig.test.json
yarn.lock
Commit message: 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

Vulnerable code:
import { WidgetTracker } from '@jupyterlab/apputils';
  optional: [ILayoutRestorer, ISettingRegistry, ITableOfContentsRegistry],
  tocRegistry: ITableOfContentsRegistry | null
        rendermime.markdownParser
import { IMarkdownParser } from '@jupyterlab/rendermime';
    protected parser: IMarkdownParser | null
          heading.level
        const selector = `h${heading.level}[id="${elementId}"]`;
        const elementId = await getIdForHeading(heading, this.parser!);
          ? `h${heading.level}[id="${elementId}"]`
  parser: IRenderMime.IMarkdownParser
      heading.level
                mdHeading.level
            `h${heading.level}[id="${id}"]`
     * Render a markdown source.
     * @returns - A promise of the string.
  parser: IMarkdownParser,
  level: number
    const innerHTML = await parser.render(raw);
    if (!innerHTML) {
      return null;
    }
    const container = document.createElement('div');
    container.innerHTML = innerHTML;
    const header = container.querySelector(`h${level}`);
    return renderMarkdown.createHeaderId(header);

Fixed code:
import { ISanitizer, WidgetTracker } from '@jupyterlab/apputils';
  IRenderMime,
  optional: [
    ILayoutRestorer,
    ISettingRegistry,
    ITableOfContentsRegistry,
    ISanitizer
  ],
  tocRegistry: ITableOfContentsRegistry | null,
  sanitizer: IRenderMime.ISanitizer | null
        rendermime.markdownParser,
        sanitizer ?? rendermime.sanitizer
import { IMarkdownParser, IRenderMime } from '@jupyterlab/rendermime';
    protected parser: IMarkdownParser | null,
    protected sanitizer: IRenderMime.ISanitizer
          heading.level,
          this.sanitizer
        const selector = `h${heading.level}[id="${CSS.escape(elementId)}"]`;
        const elementId = await getIdForHeading(
          heading,
          this.parser!,
          this.sanitizer
        );
          ? `h${heading.level}[id="${CSS.escape(elementId)}"]`
  parser: IRenderMime.IMarkdownParser,
  sanitizer: IRenderMime.ISanitizer
      heading.level,
      sanitizer
                mdHeading.level,
                this.rendermime.sanitizer
            `h${heading.level}[id="${CSS.escape(id)}"]`
     * Render a markdown source into unsanitized HTML.
     * @returns - A promise of the string containing HTML which may require sanitization.
    "@jupyterlab/rendermime-interfaces": "^3.9.0-beta.1",
import { Sanitizer } from '@jupyterlab/apputils';
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
 * @param sanitizer HTML sanitizer
  markdownParser: IMarkdownParser,
  level: number,
  sanitizer?: IRenderMime.ISanitizer
    const host = document.createElement('div');
    await renderMarkdown({
      markdownParser,
      host,
      source: raw,
      trusted: false,
      sanitizer: sanitizer ?? new Sanitizer(),
      shouldTypeset: false,
      resolver: null,
      linkHandler: null,
      latexTypesetter: null
    });
    const header = host.querySelector(`h${level}`);
    return header.id;
import { Sanitizer } from '@jupyterlab/apputils';
import { createMarkdownParser } from '@jupyterlab/markedparser-extension';
import { IMarkdownParser } from '@jupyterlab/rendermime';
import {
  EditorLanguageRegistry,
  IEditorLanguageRegistry
} from '@jupyterlab/codemirror';
    describe('#getHeadingId', () => {
      const languages: IEditorLanguageRegistry = new EditorLanguageRegistry();
      const parser: IMarkdownParser = createMarkdownParser(languages);
      const sanitizer = new Sanitizer();
      it.each<[string, string]>([
        ['# Title', 'Title'],
        [`# test'"></title><img>test {#'"><img>}`, `test'\">test-{#'\">}`]
      ])('should derive ID from markdown', async (markdown, expectedId) => {
        const headingId = await TableOfContentsUtils.Markdown.getHeadingId(
          parser,
          markdown,
          1,
          sanitizer
        );
        expect(headingId).toEqual(expectedId);
      });
    });
    },
    {
      "path": "../rendermime-interfaces"
    {
      "path": "../rendermime-interfaces"
    },
    "@jupyterlab/rendermime-interfaces": ^3.9.0-beta.1