feat: Improve iframe parameter handling (#1299)

* fix(utils): Fix TypeScript issues in `parse.ts`

* feat: Improve iframe parameter handling (modrinth/knossos#362)
- Remove not whitelisted parameters instead of not rendering whole iframe
- Allowed `start` and `end` parameters in YouTube embeds
This commit is contained in:
Norbiros
2024-07-20 02:24:29 +02:00
committed by GitHub
parent 797e1f1f21
commit 3a73994ad4

View File

@@ -1,9 +1,9 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import xss from 'xss' import { escapeAttrValue, FilterXSS, safeAttrValue, whiteList } from 'xss'
export const configuredXss = new xss.FilterXSS({ export const configuredXss = new FilterXSS({
whiteList: { whiteList: {
...xss.whiteList, ...whiteList,
summary: [], summary: [],
h1: ['id'], h1: ['id'],
h2: ['id'], h2: ['id'],
@@ -14,12 +14,12 @@ export const configuredXss = new xss.FilterXSS({
kbd: ['id'], kbd: ['id'],
input: ['checked', 'disabled', 'type'], input: ['checked', 'disabled', 'type'],
iframe: ['width', 'height', 'allowfullscreen', 'frameborder', 'start', 'end'], iframe: ['width', 'height', 'allowfullscreen', 'frameborder', 'start', 'end'],
img: [...xss.whiteList.img, 'usemap', 'style'], img: [...(whiteList.img || []), 'usemap', 'style'],
map: ['name'], map: ['name'],
area: [...xss.whiteList.a, 'coords'], area: [...(whiteList.a || []), 'coords'],
a: [...xss.whiteList.a, 'rel'], a: [...(whiteList.a || []), 'rel'],
td: [...xss.whiteList.td, 'style'], td: [...(whiteList.td || []), 'style'],
th: [...xss.whiteList.th, 'style'], th: [...(whiteList.th || []), 'style'],
picture: [], picture: [],
source: ['media', 'sizes', 'src', 'srcset', 'type'], source: ['media', 'sizes', 'src', 'srcset', 'type'],
}, },
@@ -35,35 +35,43 @@ export const configuredXss = new xss.FilterXSS({
if (tag === 'iframe' && name === 'src') { if (tag === 'iframe' && name === 'src') {
const allowedSources = [ const allowedSources = [
{ {
regex: url: /^https?:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[a-zA-Z0-9_-]{11}/,
/^https?:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\/[a-zA-Z0-9_-]{11}(\?&autoplay=[0-1]{1})?$/, allowedParameters: [/start=\d+/, /end=\d+/],
remove: ['&autoplay=1'], // Prevents autoplay
}, },
{ {
regex: /^https?:\/\/(www\.)?discord\.com\/widget\?id=\d{18,19}(&theme=\w+)?$/, url: /^https?:\/\/(www\.)?discord\.com\/widget/,
remove: [/&theme=\w+/], allowedParameters: [/id=\d{18,19}/],
}, },
] ]
const url = new URL(value)
for (const source of allowedSources) { for (const source of allowedSources) {
if (source.regex.test(value)) { if (!source.url.test(url.href)) {
for (const remove of source.remove) { continue
value = value.replace(remove, '')
}
return `${name}="${xss.escapeAttrValue(value)}"`
} }
const newSearchParams = new URLSearchParams()
url.searchParams.forEach((value, key) => {
if (!source.allowedParameters.some((param) => param.test(`${key}=${value}`))) {
newSearchParams.delete(key)
}
})
url.search = newSearchParams.toString()
return `${name}="${escapeAttrValue(url.toString())}"`
} }
} }
// For Highlight.JS // For Highlight.JS
if (name === 'class' && ['pre', 'code', 'span'].includes(tag)) { if (name === 'class' && ['pre', 'code', 'span'].includes(tag)) {
const allowedClasses = [] const allowedClasses: string[] = []
for (const className of value.split(/\s/g)) { for (const className of value.split(/\s/g)) {
if (className.startsWith('hljs-') || className.startsWith('language-')) { if (className.startsWith('hljs-') || className.startsWith('language-')) {
allowedClasses.push(className) allowedClasses.push(className)
} }
} }
return `${name}="${xss.escapeAttrValue(allowedClasses.join(' '))}"` return `${name}="${escapeAttrValue(allowedClasses.join(' '))}"`
} }
}, },
safeAttrValue(tag, name, value, cssFilter) { safeAttrValue(tag, name, value, cssFilter) {
@@ -92,7 +100,7 @@ export const configuredXss = new xss.FilterXSS({
] ]
if (!allowedHostnames.includes(url.hostname)) { if (!allowedHostnames.includes(url.hostname)) {
return xss.safeAttrValue( return safeAttrValue(
tag, tag,
name, name,
`https://wsrv.nl/?url=${encodeURIComponent( `https://wsrv.nl/?url=${encodeURIComponent(
@@ -101,13 +109,13 @@ export const configuredXss = new xss.FilterXSS({
cssFilter, cssFilter,
) )
} }
return xss.safeAttrValue(tag, name, url.toString(), cssFilter) return safeAttrValue(tag, name, url.toString(), cssFilter)
} catch (err) { } catch (err) {
/* empty */ /* empty */
} }
} }
return xss.safeAttrValue(tag, name, value, cssFilter) return safeAttrValue(tag, name, value, cssFilter)
}, },
}) })
@@ -129,7 +137,7 @@ export const md = (options = {}) => {
const token = tokens[idx] const token = tokens[idx]
const index = token.attrIndex('href') const index = token.attrIndex('href')
if (index !== -1) { if (token.attrs && index !== -1) {
const href = token.attrs[index][1] const href = token.attrs[index][1]
try { try {
@@ -152,4 +160,4 @@ export const md = (options = {}) => {
return md return md
} }
export const renderString = (string) => configuredXss.process(md().render(string)) export const renderString = (string: string) => configuredXss.process(md().render(string))