From 70579d73437166a0d85da365be855401bee37aa3 Mon Sep 17 00:00:00 2001 From: VityaSchel <59040542+VityaSchel@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:30:58 +0400 Subject: [PATCH 1/4] add html sanitization to fix xss vulnerability --- package-lock.json | 28 +++++++++++++++++++++++++++- package.json | 3 ++- src/lib/Markdown.svelte | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3679166..5a3bcf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "svelte-intersection-observer": "^0.10.1", "svelte-markdown": "^0.3.0", "svelte-timezone-picker": "^2.0.3", - "svelty-picker": "^5.2.0" + "svelty-picker": "^5.2.0", + "xss": "^1.0.15" }, "devDependencies": { "@floating-ui/dom": "^1.5.1", @@ -1700,6 +1701,11 @@ "node": ">=4" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" + }, "node_modules/d3": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", @@ -4684,6 +4690,26 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 406b568..9e1930b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "svelte-intersection-observer": "^0.10.1", "svelte-markdown": "^0.3.0", "svelte-timezone-picker": "^2.0.3", - "svelty-picker": "^5.2.0" + "svelty-picker": "^5.2.0", + "xss": "^1.0.15" }, "overrides": { "svelte-markdown": { diff --git a/src/lib/Markdown.svelte b/src/lib/Markdown.svelte index df0fd99..2b2ddc1 100644 --- a/src/lib/Markdown.svelte +++ b/src/lib/Markdown.svelte @@ -3,6 +3,7 @@ import MarkdownIt from 'markdown-it'; import { highlights } from './stores'; import { higlight } from './utils'; + import xss from 'xss' const md = new MarkdownIt(); const alphanumericRegex = /[a-zA-Z0-9]/; md.renderer.rules.image = (tokens, idx, options, env, slf) => { @@ -24,6 +25,36 @@ source = decodeHTML(source); source = md.render(source); source = decodeHTML(source); + + let content = xss(source, { + whiteList: { + pre: [], + code: [], + table: [], + blockquote: [], + br: [], + p: [], + li: [], + ol: [], + ul: [], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + b: [], + strong: [], + i: [], + em: [], + s: [], + strike: [], + img: ['src', 'alt', 'title'], + a: ['href', 'title'], + sup: [], + hr: [], + }, + }) -{@html higlight(source, $highlights)} +{@html higlight(content, $highlights)} From a18a893c6f05d0cbbe2f93fa5903b3627e58fa68 Mon Sep 17 00:00:00 2001 From: VityaSchel <59040542+VityaSchel@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:34:56 +0400 Subject: [PATCH 2/4] Add image rendering and CSS styles --- src/lib/Markdown.svelte | 128 ++++++++++++++++++++++++++++------------ src/lib/utils.ts | 61 +++++++++++++++++++ 2 files changed, 151 insertions(+), 38 deletions(-) diff --git a/src/lib/Markdown.svelte b/src/lib/Markdown.svelte index 2b2ddc1..dcef4d1 100644 --- a/src/lib/Markdown.svelte +++ b/src/lib/Markdown.svelte @@ -1,10 +1,11 @@ -{@html higlight(content, $highlights)} +
+ {@html higlight(content, $highlights)} +
+ + \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e69fe2c..f524aec 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,3 +1,5 @@ +import xss from 'xss' + export function higlight(source: string, highlights: Array) { if (highlights.length == 0) return source; if (!source) return; @@ -12,3 +14,62 @@ export function higlight(source: string, highlights: Array) { return source; } + +const zeroWidthSpaceHtmlEntity = '&#x200B;' +export function sanitizeHtml(html: string) { + return xss(html, { + whiteList: { + pre: [], + code: [], + table: [], + blockquote: [], + br: [], + p: [], + li: [], + ol: [], + ul: [], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + b: [], + strong: [], + i: [], + em: [], + s: [], + strike: [], + img: ['src', 'alt', 'title'], + a: ['href', 'title'], + sup: [], + hr: [], + }, + }).replaceAll(zeroWidthSpaceHtmlEntity, '') +} + +export const replaceRedditLinksWithImages = (html: string) => { + const domParser = new DOMParser() + const dom = domParser.parseFromString(html, 'text/html') + dom.body.querySelectorAll('a').forEach(a => { + try { + const href = new URL(a.href) + if (href.hostname === 'i.redd.it' || href.hostname === 'i.imgur.com' || href.hostname === 'preview.redd.it') { + const fig = document.createElement('figure') + const img = document.createElement('img') + const caption = document.createElement('figcaption') + img.src = a.href + if (a.textContent !== null) { + img.alt = a.textContent + } + caption.textContent = a.textContent + fig.appendChild(img) + fig.appendChild(caption) + a.replaceWith(fig) + } + } catch { + // ignore + } + }) + return dom.body.innerHTML +} \ No newline at end of file From 46ce1807d331d8e1c2855465bee29d071545e84e Mon Sep 17 00:00:00 2001 From: VityaSchel <59040542+VityaSchel@users.noreply.github.com> Date: Mon, 3 Mar 2025 20:27:08 +0400 Subject: [PATCH 3/4] fix displaying code blocks and newlines in code blocks --- src/lib/Markdown.svelte | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/lib/Markdown.svelte b/src/lib/Markdown.svelte index dcef4d1..24d3c1e 100644 --- a/src/lib/Markdown.svelte +++ b/src/lib/Markdown.svelte @@ -24,9 +24,14 @@ }; md.renderer.rules.code_inline = (tokens, idx, options, env, slf) => { const token = tokens[idx]; - let text = friendlyAttrValue(token.content); + let text = friendlyAttrValue(token.content.replaceAll(/\n/g, '
')); return `${text}`; }; + md.renderer.rules.code_block = (tokens, idx, options, env, slf) => { + const token = tokens[idx]; + let text = friendlyAttrValue(token.content.replaceAll(/\n/g, '
')); + return `
${text}
`; + }; export let source: string; $: renderedMarkdown = md.render(source); $: content = replaceRedditLinksWithImages(sanitizeHtml(renderedMarkdown)) @@ -56,15 +61,7 @@ margin-bottom: 16px; } - :global(.container code) { - border-radius: 4px; - border: 1px solid #1b1e20; - padding: 2px 4px; - font-size: 12.6px; - font-family: Noto Mono, Menlo, Monaco, Consolas, monospace; - } - - :global(.container ul, .container.ol) { + :global(.container ul, .container ol) { margin: 16px 0; margin-inline-start: 0px; margin-inline-end: 0px; @@ -109,4 +106,24 @@ text-align: center; margin-top: 4.48px; } + + :global(.container pre, .container code) { + font-family: Noto Mono, Menlo, Monaco, Consolas, monospace; + font-size: 12.6px; + border-radius: 4px; + } + + :global(.container pre) { + background: #1E1E1E; + border: 1px solid #303030; + padding: 16px 22.4px; + max-width: 100%; + overflow: auto; + white-space: pre-wrap; + } + + :global(.container code) { + border: 1px solid #1b1e20; + padding: 2px 4px; + } \ No newline at end of file From 2b700c77c778c76679e5c0e7ef5d3bc6e3db5852 Mon Sep 17 00:00:00 2001 From: VityaSchel <59040542+VityaSchel@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:14:00 +0400 Subject: [PATCH 4/4] Add support for `>! spoilers !<`, `^(sup)` and `^sup` tags --- src/lib/Markdown.svelte | 116 ++++++++++++++++++++++++++++++++++++++-- src/lib/utils.ts | 24 +++++++-- 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/src/lib/Markdown.svelte b/src/lib/Markdown.svelte index 24d3c1e..7eae5e8 100644 --- a/src/lib/Markdown.svelte +++ b/src/lib/Markdown.svelte @@ -1,7 +1,7 @@ -
+ + +
{@html higlight(content, $highlights)}
@@ -126,4 +206,32 @@ border: 1px solid #1b1e20; padding: 2px 4px; } + + :global(.container span.spoiler) { + background-color: #f1f2f2; + color: transparent; + transition: background-color 1s ease-out, color 1s ease-out; + border-radius: 2px; + cursor: pointer; + user-select: none; + } + + :global(.container span.spoiler.revealed) { + background-color: transparent; + color: inherit; + cursor: auto; + user-select: auto; + } + + :global(.container td, .container th) { + padding: 8px; + text-align: left; + font-size: 14px; + } + + :global(.container sup) { + display: inline; + vertical-align: super; + position: static; + } \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f524aec..10ea88c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -17,7 +17,7 @@ export function higlight(source: string, highlights: Array) { const zeroWidthSpaceHtmlEntity = '&#x200B;' export function sanitizeHtml(html: string) { - return xss(html, { + const sanitizedHtml = xss(html, { whiteList: { pre: [], code: [], @@ -44,13 +44,18 @@ export function sanitizeHtml(html: string) { a: ['href', 'title'], sup: [], hr: [], + span: ['class'], + thead: [], + tr: [], + th: [], + tbody: [], + td: [], }, }).replaceAll(zeroWidthSpaceHtmlEntity, '') -} -export const replaceRedditLinksWithImages = (html: string) => { const domParser = new DOMParser() - const dom = domParser.parseFromString(html, 'text/html') + const dom = domParser.parseFromString(sanitizedHtml, 'text/html') + dom.body.querySelectorAll('a').forEach(a => { try { const href = new URL(a.href) @@ -71,5 +76,16 @@ export const replaceRedditLinksWithImages = (html: string) => { // ignore } }) + + dom.body.querySelectorAll('span[class]').forEach(span => { + try { + if(span.className !== 'spoiler') { + span.className = '' + } + } catch { + // ignore + } + }) + return dom.body.innerHTML } \ No newline at end of file