diff --git a/package-lock.json b/package-lock.json index 4355e1640..b5211a3c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14507,9 +14507,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, diff --git a/src/core/render/compiler/image.js b/src/core/render/compiler/image.js index d113516b6..68bb478b4 100644 --- a/src/core/render/compiler/image.js +++ b/src/core/render/compiler/image.js @@ -1,4 +1,4 @@ -import { getAndRemoveConfig } from '../utils.js'; +import { escapeHtml, getAndRemoveConfig } from '../utils.js'; import { isAbsolutePath, getPath, getParentPath } from '../../router/util.js'; export const imageCompiler = ({ renderer, contentBase, router }) => @@ -14,7 +14,7 @@ export const imageCompiler = ({ renderer, contentBase, router }) => } if (title) { - attrs.push(`title="${title}"`); + attrs.push(`title="${escapeHtml(title)}"`); } if (config.size) { @@ -42,7 +42,7 @@ export const imageCompiler = ({ renderer, contentBase, router }) => url = getPath(contentBase, getParentPath(router.getCurrentPath()), href); } - return /* html */ `${text}`; }); diff --git a/src/core/render/compiler/link.js b/src/core/render/compiler/link.js index 44d2bcc2e..6c017adcf 100644 --- a/src/core/render/compiler/link.js +++ b/src/core/render/compiler/link.js @@ -1,4 +1,4 @@ -import { getAndRemoveConfig } from '../utils.js'; +import { escapeHtml, getAndRemoveConfig } from '../utils.js'; import { isAbsolutePath } from '../../router/util.js'; export const linkCompiler = ({ @@ -65,8 +65,8 @@ export const linkCompiler = ({ } if (title) { - attrs.push(`title="${title}"`); + attrs.push(`title="${escapeHtml(title)}"`); } - return /* html */ `${text}`; + return /* html */ `${text}`; }); diff --git a/src/core/render/compiler/media.js b/src/core/render/compiler/media.js index 3fa3cd799..a35e40e01 100644 --- a/src/core/render/compiler/media.js +++ b/src/core/render/compiler/media.js @@ -1,3 +1,5 @@ +import { escapeHtml } from '../utils'; + export const compileMedia = { markdown(url) { return { @@ -11,19 +13,19 @@ export const compileMedia = { }, iframe(url, title) { return { - html: ``, }; }, video(url, title) { return { - html: ``, + html: ``, }; }, audio(url, title) { return { - html: ``, + html: ``, }; }, code(url, title) { diff --git a/test/integration/example.test.js b/test/integration/example.test.js index 4d5d59026..fd01183ec 100644 --- a/test/integration/example.test.js +++ b/test/integration/example.test.js @@ -228,9 +228,9 @@ describe('Creating a Docsify site (integration tests in Jest)', function () { # Text between [filename](_media/example3.js ':include :fragment=something_else_not_code') - + [filename](_media/example4.js ':include :fragment=demo') - + # Text after `, }, @@ -303,4 +303,26 @@ Command | Description | Parameters expect(mainText).toContain('Something'); expect(mainText).toContain('this is include content'); }); + + test.each([ + { type: 'iframe', selector: 'iframe' }, + { type: 'video', selector: 'video' }, + { type: 'audio', selector: 'audio' }, + ])('embed %s escapes URL for XSS safety', async ({ type, selector }) => { + const dangerousUrl = 'https://example.com/?q=">'; + + await docsifyInit({ + markdown: { + homepage: `[media](${dangerousUrl} ':include :type=${type}')`, + }, + }); + + expect( + await waitForFunction(() => !!document.querySelector(selector)), + ).toBe(true); + + const mediaElm = document.querySelector(selector); + expect(mediaElm.getAttribute('src')).toBe(dangerousUrl); + expect(mediaElm.hasAttribute('onload')).toBe(false); + }); }); diff --git a/test/integration/render.test.js b/test/integration/render.test.js index 876321bb7..bb49c98e2 100644 --- a/test/integration/render.test.js +++ b/test/integration/render.test.js @@ -241,6 +241,16 @@ Text

" '"

alt text

"', ); }); + + test('escapes image alt and title to prevent attribute injection', async function () { + const output = window.marked( + '![alt" onerror="alert(1)](http://imageUrl \'title" onerror="alert(1)\')', + ); + + expect(output).not.toContain(' onerror="alert(1)"'); + expect(output).toContain('alt="alt" onerror="alert(1)"'); + expect(output).toContain('title="title" onerror="alert(1)"'); + }); }); // Headings @@ -377,6 +387,15 @@ Text

" `"

alt text

"`, ); }); + + test('escapes link title to prevent attribute injection', async function () { + const output = window.marked( + `[alt text](http://url 'title" onclick="alert(1)')`, + ); + + expect(output).not.toContain(' onclick="alert(1)"'); + expect(output).toContain('title="title" onclick="alert(1)"'); + }); }); // Skip Link