diff --git a/tests/e2e/jupyter-render.test.ts b/tests/e2e/jupyter-render.test.ts index 020c2aee04..20fe6797fe 100644 --- a/tests/e2e/jupyter-render.test.ts +++ b/tests/e2e/jupyter-render.test.ts @@ -1,6 +1,6 @@ import {env} from 'node:process'; import {expect, test} from '@playwright/test'; -import {login, apiCreateRepo, apiCreateFile, randomString} from './utils.ts'; +import {login, apiCreateRepo, apiCreateFile, assertNoJsError, randomString} from './utils.ts'; test.describe('jupyter notebook rendering', () => { let repoName: string; @@ -31,24 +31,28 @@ test.describe('jupyter notebook rendering', () => { test('renders markdown cells', async ({page}) => { await login(page); await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`); + await assertNoJsError(page); await expect(page.frameLocator('iframe.external-render-iframe').locator('.cell.markdown strong')).toBeVisible(); }); test('renders code cells with outputs', async ({page}) => { await login(page); await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`); + await assertNoJsError(page); await expect(page.frameLocator('iframe.external-render-iframe').locator('.cell.code .output pre').first()).toBeVisible(); }); test('renders image outputs', async ({page}) => { await login(page); await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`); + await assertNoJsError(page); await expect(page.frameLocator('iframe.external-render-iframe').locator('.cell.code .output img')).toBeVisible(); }); test('renders error outputs', async ({page}) => { await login(page); await page.goto(`/${owner}/${repoName}/src/branch/main/test.ipynb`); + await assertNoJsError(page); await expect(page.frameLocator('iframe.external-render-iframe').locator('.error-output')).toBeVisible(); }); }); diff --git a/web_src/css/features/jupyter.css b/web_src/css/features/jupyter.css index 49d2029cb9..41274b7782 100644 --- a/web_src/css/features/jupyter.css +++ b/web_src/css/features/jupyter.css @@ -1,4 +1,4 @@ -/* Override notebookjs default styles with Gitea theme */ +/* Gitea styles for Jupyter notebook content */ .jupyter-notebook { padding: 20px; background: var(--color-body); diff --git a/web_src/js/render/plugins/frontend-jupyter-notebook.ts b/web_src/js/render/plugins/frontend-jupyter-notebook.ts index 796b0a893f..7c30f4063c 100644 --- a/web_src/js/render/plugins/frontend-jupyter-notebook.ts +++ b/web_src/js/render/plugins/frontend-jupyter-notebook.ts @@ -1,6 +1,43 @@ import type {FrontendRenderFunc} from '../plugin.ts'; import '../../../css/features/jupyter.css'; +// Sanitize HTML by removing dangerous attributes and elements +function sanitizeHtml(element: HTMLElement) { + const dangerousAttrs = ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmousemove', + 'onmouseenter', 'onmouseleave', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onkeydown', + 'onkeyup', 'onkeypress', 'onanimationstart', 'onanimationend', 'onbegin', 'onend', 'onrepeat']; + + const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT); + const nodes: Element[] = []; + let node: Node | null; + while ((node = walker.nextNode())) { + nodes.push(node as Element); + } + + for (const el of nodes) { + // Remove all on* event handlers + for (const attr of dangerousAttrs) { + el.removeAttribute(attr); + } + + // Remove javascript: and data: URLs from href and src + const urlPattern = /^(javascript|data):/; + const href = el.getAttribute('href'); + if (href && urlPattern.test(href.toLowerCase().trim())) { + el.removeAttribute('href'); + } + const src = el.getAttribute('src'); + if (src && urlPattern.test(src.toLowerCase().trim())) { + el.removeAttribute('src'); + } + + // Remove