web/html: remove old client-side html sanitizer

This commit is contained in:
Tulir Asokan 2024-10-20 12:34:12 +03:00
parent e31bb4ebb6
commit c0a8deb347
3 changed files with 12 additions and 258 deletions

58
web/package-lock.json generated
View file

@ -11,11 +11,9 @@
"dependencies": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"linkify-react": "^4.1.3",
"react": "^19.0.0-rc-0751fac7-20241002",
"react-dom": "^19.0.0-rc-0751fac7-20241002",
"react-spinners": "^0.14.1",
"sanitize-html": "^2.13.1",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
@ -2418,14 +2416,6 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@ -2476,6 +2466,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dev": true,
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
@ -2489,6 +2480,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true,
"funding": [
{
"type": "github",
@ -2500,6 +2492,7 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dev": true,
"dependencies": {
"domelementtype": "^2.3.0"
},
@ -2514,6 +2507,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"dev": true,
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
@ -2543,6 +2537,7 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"engines": {
"node": ">=0.12"
},
@ -2743,6 +2738,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -3384,6 +3380,7 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
@ -3619,14 +3616,6 @@
"node": ">=8"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@ -3821,15 +3810,6 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
"node_modules/linkify-react": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
"integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==",
"peerDependencies": {
"linkifyjs": "^4.0.0",
"react": ">= 15.0.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -3922,6 +3902,7 @@
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
@ -4122,11 +4103,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4163,7 +4139,8 @@
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -4190,6 +4167,7 @@
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -4426,19 +4404,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sanitize-html": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz",
"integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==",
"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"
}
},
"node_modules/scheduler": {
"version": "0.25.0-rc-0751fac7-20241002",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-0751fac7-20241002.tgz",
@ -4541,6 +4506,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}

View file

@ -13,11 +13,9 @@
"dependencies": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"linkify-react": "^4.1.3",
"react": "^19.0.0-rc-0751fac7-20241002",
"react-dom": "^19.0.0-rc-0751fac7-20241002",
"react-spinners": "^0.14.1",
"sanitize-html": "^2.13.1",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {

View file

@ -1,210 +0,0 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// From matrix-react-sdk, Copyright 2024 The Matrix.org Foundation C.I.C.
// Originally licensed under the Apache License, Version 2.0
// https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/Linkify.tsx#L245
import sanitizeHtml from "sanitize-html"
import { getMediaURL } from "../api/media.ts"
import { calculateMediaSize } from "./mediasize.ts"
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/
export const PERMITTED_URL_SCHEMES = [
"bitcoin",
"ftp",
"geo",
"http",
"https",
"im",
"irc",
"ircs",
"magnet",
"mailto",
"matrix",
"mms",
"news",
"nntp",
"openpgp4fpr",
"sip",
"sftp",
"sms",
"smsto",
"ssh",
"tel",
"urn",
"webcal",
"wtai",
"xmpp",
]
export const transformTags: NonNullable<sanitizeHtml.IOptions["transformTags"]> = {
"a": function(tagName: string, attribs: sanitizeHtml.Attributes) {
if (attribs.href) {
attribs.target = "_blank"
} else {
// Delete the href attrib if it is falsy
delete attribs.href
}
attribs.rel = "noreferrer noopener" // https://mathiasbynens.github.io/rel-noopener/
return { tagName, attribs }
},
"img": function(tagName: string, attribs: sanitizeHtml.Attributes) {
const src = attribs.src
if (!src.startsWith("mxc://")) {
return {
tagName,
attribs: {},
}
}
const requestedWidth = Number(attribs.width)
const requestedHeight = Number(attribs.height)
if (requestedHeight && requestedHeight <= 48) {
attribs.style = `height: ${requestedHeight}px; width: auto; max-width: ${2 * requestedHeight}px;`
}
const style = calculateMediaSize(requestedWidth, requestedHeight)
if (style.media.aspectRatio) {
attribs.style = `width: ${style.container.width}; height: ${style.container.height};`
} else {
attribs.style = `height: 24px; width: auto; max-width: 48px;`
}
attribs.src = getMediaURL(src)!
attribs.loading = "lazy"
return { tagName, attribs }
},
"code": function(tagName: string, attribs: sanitizeHtml.Attributes) {
if (typeof attribs.class !== "undefined") {
// Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s/).filter(function(cl) {
return cl.startsWith("language-") && !cl.startsWith("language-_")
})
attribs.class = classes.join(" ")
}
return { tagName, attribs }
},
"*": function(tagName: string, attribs: sanitizeHtml.Attributes) {
// Delete any style previously assigned, style is an allowedTag for font, span & img,
// because attributes are stripped after transforming.
// For img this is trusted as it is generated wholly within the img transformation method.
if (tagName !== "img") {
delete attribs.style
}
if (tagName === "span") {
attribs.title = attribs["data-mx-spoiler"]
}
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
// equivalents
const customCSSMapper: Record<string, string> = {
"data-mx-color": "color",
"data-mx-bg-color": "background-color",
// $customAttributeKey: $cssAttributeKey
}
let style = ""
for (const [customAttributeKey, cssAttributeKey] of Object.entries(customCSSMapper)) {
const customAttributeValue = attribs[customAttributeKey]
if (
customAttributeValue &&
typeof customAttributeValue === "string" &&
COLOR_REGEX.test(customAttributeValue)
) {
style += cssAttributeKey + ":" + customAttributeValue + ";"
delete attribs[customAttributeKey]
}
}
if (style) {
attribs.style = style + (attribs.style || "")
}
return { tagName, attribs }
},
}
export const sanitizeHtmlParams: sanitizeHtml.IOptions = {
allowedTags: [
// These tags are suggested by the spec https://spec.matrix.org/v1.12/client-server-api/#mroommessage-msgtypes
"font",
"del",
"s",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"sup",
"sub",
"nl",
"li",
"b",
"i",
"u",
"strong",
"em",
"strike",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tr",
"th",
"td",
"pre",
"span",
"img",
"details",
"summary",
],
allowedAttributes: {
// attribute sanitization happens after transformations, so we have to accept `style` for font, span & img
// but strip during the transformation.
// custom ones first:
font: ["color", "data-mx-bg-color", "data-mx-color", "style"], // custom to matrix
span: [
"data-mx-maths", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler", "style", "title",
], // custom to matrix
div: ["data-mx-maths"],
// eslint-disable-next-line id-length
a: ["href", "name", "target", "rel"], // remote target: custom to matrix
// img tags also accept width/height, we just map those to max-width & max-height during transformation
img: ["src", "alt", "title", "style", "loading", "data-mx-emoticon"],
ol: ["start"],
code: ["class"], // We don't actually allow all classes, we filter them in transformTags
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ["img", "br", "hr", "area", "base", "basefont", "input", "link", "meta"],
// URL schemes we permit
allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false,
transformTags,
// 50 levels deep "should be enough for anyone"
nestingLimit: 50,
}