Skip to content

Commit

Permalink
Add client-side sanitization of server-provided HTML
Browse files Browse the repository at this point in the history
  • Loading branch information
ClearlyClaire committed Nov 3, 2023
1 parent 3bf2a72 commit b5a07a2
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 19 deletions.
11 changes: 6 additions & 5 deletions app/javascript/mastodon/actions/importer/normalizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import escapeTextContentForBrowser from 'escape-html';

import emojify from '../../features/emoji/emoji';
import { expandSpoilers } from '../../initial_state';
import { sanitize } from '../../utils/sanitize';

const domParser = new DOMParser();

Expand Down Expand Up @@ -66,8 +67,8 @@ export function normalizeStatus(status, normalOldStatus) {
const emojiMap = makeEmojiMap(normalStatus.emojis);

normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.contentHtml = sanitize(emojify(normalStatus.content, emojiMap));
normalStatus.spoilerHtml = sanitize(emojify(escapeTextContentForBrowser(spoilerText), emojiMap));
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
}

Expand All @@ -93,8 +94,8 @@ export function normalizeStatusTranslation(translation, status) {
detected_source_language: translation.detected_source_language,
language: translation.language,
provider: translation.provider,
contentHtml: emojify(translation.content, emojiMap),
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
contentHtml: sanitize(emojify(translation.content, emojiMap)),
spoilerHtml: sanitize(emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap)),
spoiler_text: translation.spoiler_text,
};

Expand Down Expand Up @@ -137,7 +138,7 @@ export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);

normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
normalAnnouncement.contentHtml = sanitize(emojify(normalAnnouncement.content, emojiMap));

return normalAnnouncement;
}
4 changes: 3 additions & 1 deletion app/javascript/mastodon/components/verified_badge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/check.svg';

import { sanitize } from 'mastodon/utils/sanitize';

import { Icon } from './icon';

const domParser = new DOMParser();
Expand All @@ -15,7 +17,7 @@ const stripRelMe = (html: string) => {
});

const body = document.querySelector('body');
return body ? { __html: body.innerHTML } : undefined;
return body ? { __html: sanitize(body.innerHTML) } : undefined;
};

interface Props {
Expand Down
3 changes: 2 additions & 1 deletion app/javascript/mastodon/features/status/components/card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useBlurhash } from 'mastodon/initial_state';
import { sanitize_oembed } from 'mastodon/utils/sanitize';

const IDNA_PREFIX = 'xn--';

Expand Down Expand Up @@ -109,7 +110,7 @@ export default class Card extends PureComponent {

renderVideo () {
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };
const content = { __html: sanitize_oembed(addAutoPlay(card.get('html'))) };

return (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import InlineAccount from 'mastodon/components/inline_account';
import MediaAttachments from 'mastodon/components/media_attachments';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import emojify from 'mastodon/features/emoji/emoji';
import { sanitize } from 'mastodon/utils/sanitize';

const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
Expand Down Expand Up @@ -51,8 +52,8 @@ class CompareHistoryModal extends PureComponent {
return obj;
}, {});

const content = { __html: emojify(currentVersion.get('content'), emojiMap) };
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap) };
const content = { __html: sanitize(emojify(currentVersion.get('content'), emojiMap)) };
const spoilerContent = { __html: sanitize(emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap)) };

const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
Expand Down Expand Up @@ -90,7 +91,7 @@ class CompareHistoryModal extends PureComponent {

<span
className='poll__option__text translate'
dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
dangerouslySetInnerHTML={{ __html: sanitize(emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)) }}
lang={language}
/>
</li>
Expand Down
21 changes: 13 additions & 8 deletions app/javascript/mastodon/models/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';
import emojify from 'mastodon/features/emoji/emoji';
import { unescapeHTML } from 'mastodon/utils/html';
import { sanitize } from 'mastodon/utils/sanitize';

import { CustomEmojiFactory } from './custom_emoji';
import type { CustomEmoji } from './custom_emoji';
Expand Down Expand Up @@ -112,11 +113,13 @@ function createAccountField(
) {
return AccountFieldFactory({
...jsonField,
name_emojified: emojify(
escapeTextContentForBrowser(jsonField.name),
emojiMap,
name_emojified: sanitize(
emojify(

Check failure on line 117 in app/javascript/mastodon/models/account.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `⏎········escapeTextContentForBrowser(jsonField.name),⏎········emojiMap,⏎······)` with `escapeTextContentForBrowser(jsonField.name),·emojiMap),`
escapeTextContentForBrowser(jsonField.name),
emojiMap,
)
),
value_emojified: emojify(jsonField.value, emojiMap),
value_emojified: sanitize(emojify(jsonField.value, emojiMap)),
value_plain: unescapeHTML(jsonField.value),
});
}
Expand All @@ -139,11 +142,13 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
),
emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))),
roles: List(serverJSON.roles?.map((role) => AccountRoleFactory(role))),
display_name_html: emojify(
escapeTextContentForBrowser(displayName),
emojiMap,
display_name_html: sanitize(
emojify(

Check failure on line 146 in app/javascript/mastodon/models/account.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `⏎········escapeTextContentForBrowser(displayName),⏎········emojiMap,⏎······)` with `escapeTextContentForBrowser(displayName),·emojiMap),`
escapeTextContentForBrowser(displayName),
emojiMap,
)
),
note_emojified: emojify(accountJSON.note, emojiMap),
note_emojified: sanitize(emojify(accountJSON.note, emojiMap)),
note_plain: unescapeHTML(accountJSON.note),
});
}
58 changes: 58 additions & 0 deletions app/javascript/mastodon/utils/sanitize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import DOMPurify from 'dompurify';

const default_config = {
ALLOWED_TAGS: [
'p',
'br',
'span',
'a',
'del',
'pre',
'blockquote',
'code',
'b',
'strong',
'u',
'i',
'em',
'ul',
'ol',
'li',
'img',
],
ALLOWED_ATTR: [
'src',
'alt',
'title',
'draggable',
'href',
'rel',
'class',
'translate',
'start',
'reversed',
'value',
'target',
],
};

const oembed_config = {
ALLOWED_TAGS: ['audio', 'embed', 'iframe', 'source', 'video'],
ALLOWED_ATTR: [
'controls',
'width',
'height',
'src',
'type',
'allowfullscreen',
'frameborder',
'scrolling',
'loop',
'sandbox',
],
};

export const sanitize = (src: string) =>
DOMPurify.sanitize(src, default_config);
export const sanitize_oembed = (src: string) =>
DOMPurify.sanitize(src, oembed_config);
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"css-loader": "^5.2.7",
"cssnano": "^6.0.1",
"detect-passive-events": "^2.0.3",
"dompurify": "^3.0.5",
"dotenv": "^16.0.3",
"emoji-mart": "npm:emoji-mart-lazyload@latest",
"escape-html": "^1.0.3",
Expand Down Expand Up @@ -155,6 +156,7 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/babel__core": "^7.20.1",
"@types/dompurify": "^3.0.2",
"@types/emoji-mart": "^3.0.9",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17",
Expand Down
14 changes: 13 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,13 @@
dependencies:
"@types/node" "*"

"@types/dompurify@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.2.tgz#c1cd33a475bc49c43c2a7900e41028e2136a4553"
integrity sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==
dependencies:
"@types/trusted-types" "*"

"@types/emoji-mart@^3.0.9":
version "3.0.11"
resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-3.0.11.tgz#4b0f9b52dd191df13928779b55aacb2c88d201ba"
Expand Down Expand Up @@ -2587,7 +2594,7 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==

"@types/trusted-types@^2.0.2":
"@types/trusted-types@*", "@types/trusted-types@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
Expand Down Expand Up @@ -5108,6 +5115,11 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"

dompurify@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.5.tgz#eb3d9cfa10037b6e73f32c586682c4b2ab01fbed"
integrity sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==

domutils@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
Expand Down

0 comments on commit b5a07a2

Please sign in to comment.