From 4aa6fdb4dd272afe2e56bbadbe5b3f443ecf215c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 17 Jan 2024 12:21:19 -0600 Subject: [PATCH 1/4] Add DOMPurify --- package.json | 2 ++ .../landing-timeline/components/site-banner.tsx | 3 ++- src/normalizers/status-edit.ts | 7 ++++--- src/reducers/statuses.ts | 5 +++-- src/schemas/account.ts | 7 ++++--- src/schemas/poll.ts | 3 ++- yarn.lock | 17 +++++++++++++++++ 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index ba82523fb5..7b2c0e0288 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@tanstack/react-query": "^5.0.0", + "@types/dompurify": "^3.0.5", "@types/escape-html": "^1.0.1", "@types/http-link-header": "^1.0.3", "@types/leaflet": "^1.8.0", @@ -106,6 +107,7 @@ "cryptocurrency-icons": "^0.18.1", "cssnano": "^6.0.0", "detect-passive-events": "^2.0.0", + "dompurify": "^3.0.8", "dotenv": "^16.0.0", "emoji-datasource": "14.0.0", "emoji-mart": "^5.5.2", diff --git a/src/features/landing-timeline/components/site-banner.tsx b/src/features/landing-timeline/components/site-banner.tsx index 194faecc64..44776a43fa 100644 --- a/src/features/landing-timeline/components/site-banner.tsx +++ b/src/features/landing-timeline/components/site-banner.tsx @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import React from 'react'; import Markup from 'soapbox/components/markup'; @@ -9,7 +10,7 @@ import { LogoText } from './logo-text'; const SiteBanner: React.FC = () => { const instance = useInstance(); - const description = instance.description; + const description = DOMPurify.sanitize(instance.description); return ( diff --git a/src/normalizers/status-edit.ts b/src/normalizers/status-edit.ts index f569ecce3f..40e5acd351 100644 --- a/src/normalizers/status-edit.ts +++ b/src/normalizers/status-edit.ts @@ -1,6 +1,7 @@ /** * Status edit normalizer - */ +*/ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, @@ -60,8 +61,8 @@ const normalizeStatusPoll = (statusEdit: ImmutableMap) => { const normalizeContent = (statusEdit: ImmutableMap) => { const emojiMap = makeEmojiMap(statusEdit.get('emojis')); - const contentHtml = stripCompatibilityFeatures(emojify(statusEdit.get('content'), emojiMap)); - const spoilerHtml = emojify(escapeTextContentForBrowser(statusEdit.get('spoiler_text')), emojiMap); + const contentHtml = DOMPurify.sanitize(stripCompatibilityFeatures(emojify(statusEdit.get('content'), emojiMap)), { ADD_ATTR: ['target'] }); + const spoilerHtml = DOMPurify.sanitize(emojify(escapeTextContentForBrowser(statusEdit.get('spoiler_text')), emojiMap), { ADD_ATTR: ['target'] }); return statusEdit .set('contentHtml', contentHtml) diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 37d7ec2add..4eff745065 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -117,8 +118,8 @@ export const calculateStatus = ( return status.merge({ search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '', - contentHtml: stripCompatibilityFeatures(emojify(status.content, emojiMap)), - spoilerHtml: emojify(escapeTextContentForBrowser(spoilerText), emojiMap), + contentHtml: DOMPurify.sanitize(stripCompatibilityFeatures(emojify(status.content, emojiMap)), { USE_PROFILES: { html: true } }), + spoilerHtml: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(spoilerText), emojiMap), { USE_PROFILES: { html: true } }), hidden: expandSpoilers ? false : spoilerText.length > 0 || status.sensitive, }); } diff --git a/src/schemas/account.ts b/src/schemas/account.ts index fec6c18b2f..555badab0f 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import z from 'zod'; @@ -112,7 +113,7 @@ const transformAccount = ({ pleroma, other_setti const newFields = fields.map((field) => ({ ...field, - name_emojified: emojify(escapeTextContentForBrowser(field.name), customEmojiMap), + name_emojified: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(field.name), customEmojiMap), { USE_PROFILES: { html: true } }), value_emojified: emojify(field.value, customEmojiMap), value_plain: unescapeHTML(field.value), })); @@ -130,7 +131,7 @@ const transformAccount = ({ pleroma, other_setti avatar_static: account.avatar_static || account.avatar, discoverable: account.discoverable || account.source?.pleroma?.discoverable || false, display_name: displayName, - display_name_html: emojify(escapeTextContentForBrowser(displayName), customEmojiMap), + display_name_html: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(displayName), customEmojiMap), { USE_PROFILES: { html: true } }), domain, fields: newFields, fqn: account.fqn || (account.acct.includes('@') ? account.acct : `${account.acct}@${domain}`), @@ -138,7 +139,7 @@ const transformAccount = ({ pleroma, other_setti moderator: pleroma?.is_moderator || false, local: pleroma?.is_local !== undefined ? pleroma.is_local : account.acct.split('@')[1] === undefined, location: account.location || pleroma?.location || other_settings?.location || '', - note_emojified: emojify(account.note, customEmojiMap), + note_emojified: DOMPurify.sanitize(emojify(account.note, customEmojiMap), { USE_PROFILES: { html: true } }), pleroma: (() => { if (!pleroma) return undefined; const { relationship, ...rest } = pleroma; diff --git a/src/schemas/poll.ts b/src/schemas/poll.ts index 65ead6e149..a3012b1786 100644 --- a/src/schemas/poll.ts +++ b/src/schemas/poll.ts @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { z } from 'zod'; @@ -30,7 +31,7 @@ const pollSchema = z.object({ const emojifiedOptions = poll.options.map((option) => ({ ...option, - title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + title_emojified: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(option.title), emojiMap), { ALLOWED_TAGS: [] }), })); // If the user has votes, they have certainly voted. diff --git a/yarn.lock b/yarn.lock index 26b5253bd0..ec70d92346 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2375,6 +2375,13 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@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/escape-html@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.1.tgz#b19b4646915f0ae2c306bf984dc0a59c5cfc97ba" @@ -2612,6 +2619,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564" integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw== +"@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/trusted-types@^2.0.2": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65" @@ -4096,6 +4108,11 @@ domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +dompurify@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.8.tgz#e0021ab1b09184bc8af7e35c7dd9063f43a8a437" + integrity sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ== + domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" From d576b8ed47d4623920302f8840592ce7be4c43b1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 8 Feb 2024 15:01:20 -0600 Subject: [PATCH 2/4] Switch to isomorphic-dompurify --- package.json | 2 +- .../components/site-banner.tsx | 2 +- src/normalizers/status-edit.ts | 2 +- src/reducers/statuses.ts | 2 +- src/schemas/account.ts | 2 +- src/schemas/poll.ts | 2 +- yarn.lock | 48 +++++++++++++++++++ 7 files changed, 54 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c71e853dc2..3a6a1b2a69 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,6 @@ "cryptocurrency-icons": "^0.18.1", "cssnano": "^6.0.0", "detect-passive-events": "^2.0.0", - "dompurify": "^3.0.8", "dotenv": "^16.0.0", "emoji-datasource": "14.0.0", "emoji-mart": "^5.5.2", @@ -126,6 +125,7 @@ "intersection-observer": "^0.12.2", "intl-messageformat": "10.5.8", "intl-pluralrules": "^2.0.0", + "isomorphic-dompurify": "^2.3.0", "leaflet": "^1.8.0", "lexical": "^0.12.4", "line-awesome": "^1.3.0", diff --git a/src/features/landing-timeline/components/site-banner.tsx b/src/features/landing-timeline/components/site-banner.tsx index 44776a43fa..ffbdafe354 100644 --- a/src/features/landing-timeline/components/site-banner.tsx +++ b/src/features/landing-timeline/components/site-banner.tsx @@ -1,4 +1,4 @@ -import * as DOMPurify from 'dompurify'; +import DOMPurify from 'isomorphic-dompurify'; import React from 'react'; import Markup from 'soapbox/components/markup'; diff --git a/src/normalizers/status-edit.ts b/src/normalizers/status-edit.ts index 40e5acd351..a72e473acf 100644 --- a/src/normalizers/status-edit.ts +++ b/src/normalizers/status-edit.ts @@ -1,7 +1,6 @@ /** * Status edit normalizer */ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, @@ -9,6 +8,7 @@ import { Record as ImmutableRecord, fromJS, } from 'immutable'; +import DOMPurify from 'isomorphic-dompurify'; import emojify from 'soapbox/features/emoji'; import { normalizeAttachment } from 'soapbox/normalizers/attachment'; diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 9083e4e664..b857ba5403 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -1,6 +1,6 @@ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import DOMPurify from 'isomorphic-dompurify'; import emojify from 'soapbox/features/emoji'; import { normalizeStatus } from 'soapbox/normalizers'; diff --git a/src/schemas/account.ts b/src/schemas/account.ts index 014820d340..b419b13cf2 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -1,5 +1,5 @@ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; +import DOMPurify from 'isomorphic-dompurify'; import z from 'zod'; import emojify from 'soapbox/features/emoji'; diff --git a/src/schemas/poll.ts b/src/schemas/poll.ts index a3012b1786..1a60af410a 100644 --- a/src/schemas/poll.ts +++ b/src/schemas/poll.ts @@ -1,5 +1,5 @@ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; +import DOMPurify from 'isomorphic-dompurify'; import { z } from 'zod'; import emojify from 'soapbox/features/emoji'; diff --git a/yarn.lock b/yarn.lock index 7906e8f98d..1235e8045e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3897,6 +3897,13 @@ cssstyle@^3.0.0: dependencies: rrweb-cssom "^0.6.0" +cssstyle@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.0.1.tgz#ef29c598a1e90125c870525490ea4f354db0660a" + integrity sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ== + dependencies: + rrweb-cssom "^0.6.0" + csstype@^3.0.2: version "3.0.9" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" @@ -5716,6 +5723,15 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isomorphic-dompurify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.3.0.tgz#bc48fbdf52f84cf7e0a63a5e8ec89052e7dbc3c5" + integrity sha512-FCoKY4/mW/jnn/+VgE7wXGC2D/RXzVCAmGYuGWEuZXtyWnwmE2100caciIv+RbHk90q9LA0OW5IBn2f+ywHtww== + dependencies: + "@types/dompurify" "^3.0.5" + dompurify "^3.0.8" + jsdom "^24.0.0" + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -5809,6 +5825,33 @@ jsdom@^23.0.0: ws "^8.14.2" xml-name-validator "^5.0.0" +jsdom@^24.0.0: + version "24.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.0.0.tgz#e2dc04e4c79da368481659818ee2b0cd7c39007c" + integrity sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A== + dependencies: + cssstyle "^4.0.1" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.2" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.7" + parse5 "^7.1.2" + rrweb-cssom "^0.6.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.3" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.16.0" + xml-name-validator "^5.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -9500,6 +9543,11 @@ ws@^8.14.2: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +ws@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + xcase@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9" From d7e0ded57a77bf04ae1e6beb2abe08f3479e6ef4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 8 Feb 2024 15:02:26 -0600 Subject: [PATCH 3/4] Remove @types/dompurify --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3a6a1b2a69..a3150d8551 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@tanstack/react-query": "^5.0.0", - "@types/dompurify": "^3.0.5", "@types/escape-html": "^1.0.1", "@types/http-link-header": "^1.0.3", "@types/leaflet": "^1.8.0", From 694af98d21280149ad41027d3800fbefa0bfe663 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 8 Feb 2024 15:05:06 -0600 Subject: [PATCH 4/4] Remove space that was added for no reason --- src/normalizers/status-edit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/normalizers/status-edit.ts b/src/normalizers/status-edit.ts index a72e473acf..26619c1747 100644 --- a/src/normalizers/status-edit.ts +++ b/src/normalizers/status-edit.ts @@ -1,6 +1,6 @@ /** * Status edit normalizer -*/ + */ import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap,