Replace sanitize-html by dompurify in frontend

It's lighter and we don't have native dependencies warnings in web
browser
This commit is contained in:
Chocobozzz 2024-08-16 10:30:21 +02:00
parent 38cc3910ff
commit 16d9204ea8
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
7 changed files with 127 additions and 73 deletions

View File

@ -69,6 +69,7 @@
"@types/chart.js": "^2.9.37",
"@types/core-js": "^2.5.2",
"@types/debug": "^4.1.5",
"@types/dompurify": "^3.0.5",
"@types/jschannel": "^1.0.0",
"@types/linkifyjs": "^2.1.2",
"@types/lodash-es": "^4.17.0",
@ -93,6 +94,7 @@
"chartjs-plugin-zoom": "~2.0.1",
"core-js": "^3.22.8",
"debug": "^4.3.1",
"dompurify": "^3.1.6",
"eslint": "^8.28.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsdoc": "^48.1.0",
@ -109,7 +111,6 @@
"ngx-uploadx": "^6.1.0",
"primeng": "^17.3.1",
"rxjs": "^7.3.0",
"sanitize-html": "^2.1.2",
"sha.js": "^2.4.11",
"socket.io-client": "^4.5.4",
"stylelint": "^16.2.1",

View File

@ -1,41 +1,93 @@
import { Injectable } from '@angular/core'
import { getCustomMarkupSanitizeOptions, getDefaultSanitizeOptions } from '@peertube/peertube-core-utils'
import {
getDefaultSanitizedHrefAttributes,
getDefaultSanitizedSchemes,
getDefaultSanitizedTags
} from '@peertube/peertube-core-utils'
import DOMPurify, { DOMPurifyI } from 'dompurify'
import { LinkifierService } from './linkifier.service'
@Injectable()
export class HtmlRendererService {
private sanitizeHtml: typeof import ('sanitize-html')
private simpleDomPurify: DOMPurifyI
private enhancedDomPurify: DOMPurifyI
constructor (private linkifier: LinkifierService) {
this.simpleDomPurify = DOMPurify()
this.enhancedDomPurify = DOMPurify()
this.addHrefHook(this.simpleDomPurify)
this.addHrefHook(this.enhancedDomPurify)
this.addCheckSchemesHook(this.simpleDomPurify, getDefaultSanitizedSchemes())
this.addCheckSchemesHook(this.simpleDomPurify, [ ...getDefaultSanitizedSchemes(), 'mailto' ])
}
async convertToBr (text: string) {
await this.loadSanitizeHtml()
private addHrefHook (dompurifyInstance: DOMPurifyI) {
dompurifyInstance.addHook('afterSanitizeAttributes', node => {
if ('target' in node) {
node.setAttribute('target', '_blank')
const html = text.replace(/\r?\n/g, '<br />')
const rel = node.hasAttribute('rel')
? node.getAttribute('rel') + ' '
: ''
return this.sanitizeHtml(html, {
allowedTags: [ 'br' ]
node.setAttribute('rel', rel + 'noopener noreferrer')
}
})
}
async toSafeHtml (text: string, additionalAllowedTags: string[] = []) {
const [ html ] = await Promise.all([
// Convert possible markdown to html
this.linkifier.linkify(text),
private addCheckSchemesHook (dompurifyInstance: DOMPurifyI, schemes: string[]) {
const regex = new RegExp(`^(${schemes.join('|')}):`, 'im')
this.loadSanitizeHtml()
])
dompurifyInstance.addHook('afterSanitizeAttributes', node => {
const anchor = document.createElement('a')
const options = additionalAllowedTags.length !== 0
? getCustomMarkupSanitizeOptions(additionalAllowedTags)
: getDefaultSanitizeOptions()
if (node.hasAttribute('href')) {
anchor.href = node.getAttribute('href')
return this.sanitizeHtml(html, options)
if (anchor.protocol && !anchor.protocol.match(regex)) {
node.removeAttribute('href')
}
}
})
}
private async loadSanitizeHtml () {
this.sanitizeHtml = (await import('sanitize-html')).default
convertToBr (text: string) {
const html = text.replace(/\r?\n/g, '<br />')
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [ 'br' ]
})
}
async toSimpleSafeHtml (text: string) {
const html = await this.linkifier.linkify(text)
return this.sanitize(this.simpleDomPurify, html)
}
async toCustomPageSafeHtml (text: string, additionalAllowedTags: string[] = []) {
const html = await this.linkifier.linkify(text)
const enhancedTags = [ 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' ]
return this.sanitize(this.enhancedDomPurify, html, {
additionalTags: [ ...enhancedTags, ...additionalAllowedTags ],
additionalAttributes: [ 'src', 'alt', 'style' ]
})
}
private sanitize (domPurify: DOMPurifyI, html: string, options: {
additionalTags?: string[]
additionalAttributes?: string[]
} = {}) {
const { additionalTags = [], additionalAttributes = [] } = options
return domPurify.sanitize(html, {
ALLOWED_TAGS: [ ...getDefaultSanitizedTags(), ...additionalTags ],
ALLOWED_ATTR: [ ...getDefaultSanitizedHrefAttributes(), ...additionalAttributes ],
ALLOW_DATA_ATTR: true
})
}
}

View File

@ -1,4 +1,3 @@
import MarkdownIt from 'markdown-it'
import { Injectable } from '@angular/core'
import {
buildVideoLink,
@ -9,6 +8,7 @@ import {
TEXT_RULES,
TEXT_WITH_HTML_RULES
} from '@peertube/peertube-core-utils'
import MarkdownIt from 'markdown-it'
import { HtmlRendererService } from './html-renderer.service'
type MarkdownParsers = {
@ -140,7 +140,13 @@ export class MarkdownService {
const html = this.markdownParsers[name].render(markdown)
if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags)
if (config.escape) {
if (name === 'customPageMarkdownIt') {
return this.htmlRenderer.toCustomPageSafeHtml(html, additionalAllowedTags)
}
return this.htmlRenderer.toSimpleSafeHtml(html)
}
return html
}

View File

@ -67,7 +67,7 @@ export class ConfirmComponent implements OnInit {
this.confirmButtonText = confirmButtonText || $localize`Confirm`
this.html.toSafeHtml(message)
this.html.toSimpleSafeHtml(message)
.then(html => {
this.message = html

View File

@ -113,12 +113,12 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
private loadMessages () {
this.abuseService.listAbuseMessages(this.abuse)
.subscribe({
next: async res => {
next: res => {
this.abuseMessages = []
for (const m of res.data) {
this.abuseMessages.push(Object.assign(m, {
messageHtml: await this.htmlRenderer.convertToBr(m.message)
messageHtml: this.htmlRenderer.convertToBr(m.message)
}))
}

View File

@ -2806,6 +2806,13 @@
dependencies:
"@types/ms" "*"
"@types/dompurify@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
dependencies:
"@types/trusted-types" "*"
"@types/eslint-scope@^3.7.3":
version "3.7.7"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
@ -3071,6 +3078,11 @@
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
"@types/trusted-types@*":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/video.js@^7.3.40":
version "7.3.58"
resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.58.tgz#7e8cdafee25c75d6eb18f530b93ac52edff53c03"
@ -5166,11 +5178,6 @@ deepmerge-ts@^5.0.0, deepmerge-ts@^5.1.0:
resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a"
integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==
deepmerge@^4.2.2:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
default-browser-id@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26"
@ -5386,6 +5393,11 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
dompurify@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2"
integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==
domutils@^3.0.1:
version "3.1.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
@ -9248,11 +9260,6 @@ parse-node-version@^1.0.1:
resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
parse-srcset@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
parse5-html-rewriting-stream@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36"
@ -9498,7 +9505,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.4.38, postcss@^8.2.14, postcss@^8.3.11, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.38:
postcss@8.4.38, postcss@^8.2.14, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.38:
version "8.4.38"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
@ -10164,18 +10171,6 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sanitize-html@^2.1.2:
version "2.13.0"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.0.tgz#71aedcdb777897985a4ea1877bf4f895a1170dae"
integrity sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==
dependencies:
deepmerge "^4.2.2"
escape-string-regexp "^4.0.0"
htmlparser2 "^8.0.0"
is-plain-object "^5.0.0"
parse-srcset "^1.0.2"
postcss "^8.3.11"
sass-loader@14.2.1:
version "14.2.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-14.2.1.tgz#db9ad96b56dc1c1ea546101e76375d5b008fec70"

View File

@ -1,11 +1,30 @@
export function getDefaultSanitizedTags () {
return [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ]
}
export function getDefaultSanitizedSchemes () {
return [ 'http', 'https' ]
}
export function getDefaultSanitizedHrefAttributes () {
return [ 'href', 'class', 'target', 'rel' ]
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// sanitize-html
// ---------------------------------------------------------------------------
export function getDefaultSanitizeOptions () {
return {
allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
allowedSchemes: [ 'http', 'https' ],
allowedTags: getDefaultSanitizedTags(),
allowedSchemes: getDefaultSanitizedSchemes(),
allowedAttributes: {
'a': [ 'href', 'class', 'target', 'rel' ],
'a': getDefaultSanitizedHrefAttributes(),
'*': [ 'data-*' ]
},
transformTags: {
a: (tagName: string, attribs: any) => {
let rel = 'noopener noreferrer'
@ -29,28 +48,9 @@ export function getTextOnlySanitizeOptions () {
}
}
export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) {
const base = getDefaultSanitizeOptions()
return {
allowedTags: [
...base.allowedTags,
...additionalAllowedTags,
'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img'
],
allowedSchemes: [
...base.allowedSchemes,
'mailto'
],
allowedAttributes: {
...base.allowedAttributes,
'img': [ 'src', 'alt' ],
'*': [ 'data-*', 'style' ]
}
}
}
// ---------------------------------------------------------------------------
// Manual escapes
// ---------------------------------------------------------------------------
// Thanks: https://stackoverflow.com/a/12034334
export function escapeHTML (stringParam: string) {