mirror of
https://github.com/mastodon/mastodon.git
synced 2025-02-06 15:05:07 +00:00
Add client-side sanitization of server-provided HTML
This commit is contained in:
parent
9b8d1fb6d1
commit
89b53d8c9e
|
@ -4,6 +4,7 @@ import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
||||||
|
|
||||||
import emojify from '../../features/emoji/emoji';
|
import emojify from '../../features/emoji/emoji';
|
||||||
import { expandSpoilers } from '../../initial_state';
|
import { expandSpoilers } from '../../initial_state';
|
||||||
|
import { sanitize } from '../../utils/sanitize';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
@ -74,8 +75,8 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = sanitize(emojify(normalStatus.content, emojiMap));
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = sanitize(emojify(escapeTextContentForBrowser(spoilerText), emojiMap));
|
||||||
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,8 +102,8 @@ export function normalizeStatusTranslation(translation, status) {
|
||||||
detected_source_language: translation.detected_source_language,
|
detected_source_language: translation.detected_source_language,
|
||||||
language: translation.language,
|
language: translation.language,
|
||||||
provider: translation.provider,
|
provider: translation.provider,
|
||||||
contentHtml: emojify(translation.content, emojiMap),
|
contentHtml: sanitize(emojify(translation.content, emojiMap)),
|
||||||
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
|
spoilerHtml: sanitize(emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap)),
|
||||||
spoiler_text: translation.spoiler_text,
|
spoiler_text: translation.spoiler_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ export function normalizeAnnouncement(announcement) {
|
||||||
const normalAnnouncement = { ...announcement };
|
const normalAnnouncement = { ...announcement };
|
||||||
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
||||||
|
|
||||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
normalAnnouncement.contentHtml = sanitize(emojify(normalAnnouncement.content, emojiMap));
|
||||||
|
|
||||||
return normalAnnouncement;
|
return normalAnnouncement;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
import { sanitize } from 'mastodon/utils/sanitize';
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ const stripRelMe = (html: string) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = document.querySelector('body');
|
const body = document.querySelector('body');
|
||||||
return body ? { __html: body.innerHTML } : undefined;
|
return body ? { __html: sanitize(body.innerHTML) } : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { Icon } from 'mastodon/components/icon';
|
||||||
import { MoreFromAuthor } from 'mastodon/components/more_from_author';
|
import { MoreFromAuthor } from 'mastodon/components/more_from_author';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
import { useBlurhash } from 'mastodon/initial_state';
|
import { useBlurhash } from 'mastodon/initial_state';
|
||||||
|
import { sanitize_oembed } from 'mastodon/utils/sanitize';
|
||||||
|
|
||||||
const IDNA_PREFIX = 'xn--';
|
const IDNA_PREFIX = 'xn--';
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@ export default class Card extends PureComponent {
|
||||||
|
|
||||||
renderVideo () {
|
renderVideo () {
|
||||||
const { card } = this.props;
|
const { card } = this.props;
|
||||||
const content = { __html: addAutoPlay(card.get('html')) };
|
const content = { __html: sanitize_oembed(addAutoPlay(card.get('html'))) };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -15,6 +15,7 @@ import InlineAccount from 'mastodon/components/inline_account';
|
||||||
import MediaAttachments from 'mastodon/components/media_attachments';
|
import MediaAttachments from 'mastodon/components/media_attachments';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
import emojify from 'mastodon/features/emoji/emoji';
|
||||||
|
import { sanitize } from 'mastodon/utils/sanitize';
|
||||||
|
|
||||||
const mapStateToProps = (state, { statusId }) => ({
|
const mapStateToProps = (state, { statusId }) => ({
|
||||||
language: state.getIn(['statuses', statusId, 'language']),
|
language: state.getIn(['statuses', statusId, 'language']),
|
||||||
|
@ -51,8 +52,8 @@ class CompareHistoryModal extends PureComponent {
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const content = { __html: emojify(currentVersion.get('content'), emojiMap) };
|
const content = { __html: sanitize(emojify(currentVersion.get('content'), emojiMap)) };
|
||||||
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap) };
|
const spoilerContent = { __html: sanitize(emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap)) };
|
||||||
|
|
||||||
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
||||||
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
||||||
|
@ -90,7 +91,7 @@ class CompareHistoryModal extends PureComponent {
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className='poll__option__text translate'
|
className='poll__option__text translate'
|
||||||
dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
|
dangerouslySetInnerHTML={{ __html: sanitize(emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)) }}
|
||||||
lang={language}
|
lang={language}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
} from 'mastodon/api_types/accounts';
|
} from 'mastodon/api_types/accounts';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
import emojify from 'mastodon/features/emoji/emoji';
|
||||||
import { unescapeHTML } from 'mastodon/utils/html';
|
import { unescapeHTML } from 'mastodon/utils/html';
|
||||||
|
import { sanitize } from 'mastodon/utils/sanitize';
|
||||||
|
|
||||||
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
|
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
|
||||||
import type { CustomEmoji, EmojiMap } from './custom_emoji';
|
import type { CustomEmoji, EmojiMap } from './custom_emoji';
|
||||||
|
@ -107,11 +108,10 @@ function createAccountField(
|
||||||
) {
|
) {
|
||||||
return AccountFieldFactory({
|
return AccountFieldFactory({
|
||||||
...jsonField,
|
...jsonField,
|
||||||
name_emojified: emojify(
|
name_emojified: sanitize(
|
||||||
escapeTextContentForBrowser(jsonField.name),
|
emojify(escapeTextContentForBrowser(jsonField.name), emojiMap),
|
||||||
emojiMap,
|
|
||||||
),
|
),
|
||||||
value_emojified: emojify(jsonField.value, emojiMap),
|
value_emojified: sanitize(emojify(jsonField.value, emojiMap)),
|
||||||
value_plain: unescapeHTML(jsonField.value),
|
value_plain: unescapeHTML(jsonField.value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -138,11 +138,10 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
roles: ImmutableList(
|
roles: ImmutableList(
|
||||||
serverJSON.roles?.map((role) => AccountRoleFactory(role)),
|
serverJSON.roles?.map((role) => AccountRoleFactory(role)),
|
||||||
),
|
),
|
||||||
display_name_html: emojify(
|
display_name_html: sanitize(
|
||||||
escapeTextContentForBrowser(displayName),
|
emojify(escapeTextContentForBrowser(displayName), emojiMap),
|
||||||
emojiMap,
|
|
||||||
),
|
),
|
||||||
note_emojified: emojify(accountJSON.note, emojiMap),
|
note_emojified: sanitize(emojify(accountJSON.note, emojiMap)),
|
||||||
note_plain: unescapeHTML(accountJSON.note),
|
note_plain: unescapeHTML(accountJSON.note),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
58
app/javascript/mastodon/utils/sanitize.ts
Normal file
58
app/javascript/mastodon/utils/sanitize.ts
Normal file
|
@ -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);
|
|
@ -71,6 +71,7 @@
|
||||||
"css-loader": "^5.2.7",
|
"css-loader": "^5.2.7",
|
||||||
"cssnano": "^7.0.0",
|
"cssnano": "^7.0.0",
|
||||||
"detect-passive-events": "^2.0.3",
|
"detect-passive-events": "^2.0.3",
|
||||||
|
"dompurify": "^3.0.5",
|
||||||
"emoji-mart": "npm:emoji-mart-lazyload@latest",
|
"emoji-mart": "npm:emoji-mart-lazyload@latest",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
@ -145,6 +146,7 @@
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@types/babel__core": "^7.20.1",
|
"@types/babel__core": "^7.20.1",
|
||||||
|
"@types/dompurify": "^3.0.2",
|
||||||
"@types/emoji-mart": "^3.0.9",
|
"@types/emoji-mart": "^3.0.9",
|
||||||
"@types/escape-html": "^1.0.2",
|
"@types/escape-html": "^1.0.2",
|
||||||
"@types/hoist-non-react-statics": "^3.3.1",
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -2838,6 +2838,7 @@ __metadata:
|
||||||
"@testing-library/jest-dom": "npm:^6.0.0"
|
"@testing-library/jest-dom": "npm:^6.0.0"
|
||||||
"@testing-library/react": "npm:^16.0.0"
|
"@testing-library/react": "npm:^16.0.0"
|
||||||
"@types/babel__core": "npm:^7.20.1"
|
"@types/babel__core": "npm:^7.20.1"
|
||||||
|
"@types/dompurify": "npm:^3.0.2"
|
||||||
"@types/emoji-mart": "npm:^3.0.9"
|
"@types/emoji-mart": "npm:^3.0.9"
|
||||||
"@types/escape-html": "npm:^1.0.2"
|
"@types/escape-html": "npm:^1.0.2"
|
||||||
"@types/hoist-non-react-statics": "npm:^3.3.1"
|
"@types/hoist-non-react-statics": "npm:^3.3.1"
|
||||||
|
@ -2887,6 +2888,7 @@ __metadata:
|
||||||
css-loader: "npm:^5.2.7"
|
css-loader: "npm:^5.2.7"
|
||||||
cssnano: "npm:^7.0.0"
|
cssnano: "npm:^7.0.0"
|
||||||
detect-passive-events: "npm:^2.0.3"
|
detect-passive-events: "npm:^2.0.3"
|
||||||
|
dompurify: "npm:^3.0.5"
|
||||||
emoji-mart: "npm:emoji-mart-lazyload@latest"
|
emoji-mart: "npm:emoji-mart-lazyload@latest"
|
||||||
escape-html: "npm:^1.0.3"
|
escape-html: "npm:^1.0.3"
|
||||||
eslint: "npm:^8.41.0"
|
eslint: "npm:^8.41.0"
|
||||||
|
@ -3730,6 +3732,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/dompurify@npm:^3.0.2":
|
||||||
|
version: 3.0.5
|
||||||
|
resolution: "@types/dompurify@npm:3.0.5"
|
||||||
|
dependencies:
|
||||||
|
"@types/trusted-types": "npm:*"
|
||||||
|
checksum: 10c0/a34dcc4498ca250815ccf9aecbe82df96ba5db247d0440cf266a876757d47c52519c240db3475e794d7deb0d6b1af23328e02879be368ad0e26b20c0f0865dba
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/emoji-mart@npm:^3.0.9":
|
"@types/emoji-mart@npm:^3.0.9":
|
||||||
version: 3.0.14
|
version: 3.0.14
|
||||||
resolution: "@types/emoji-mart@npm:3.0.14"
|
resolution: "@types/emoji-mart@npm:3.0.14"
|
||||||
|
@ -4233,6 +4244,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/trusted-types@npm:*":
|
||||||
|
version: 2.0.6
|
||||||
|
resolution: "@types/trusted-types@npm:2.0.6"
|
||||||
|
checksum: 10c0/8d942c25bfabd89463170e22f0b3312b776885735a9c259495266b90c590f040b2112cb25e05cc2dee6e397301597b979b8ea8b0d10f2232adf38c542a16324b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/trusted-types@npm:^2.0.2":
|
"@types/trusted-types@npm:^2.0.2":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "@types/trusted-types@npm:2.0.3"
|
resolution: "@types/trusted-types@npm:2.0.3"
|
||||||
|
@ -7502,6 +7520,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"dompurify@npm:^3.0.5":
|
||||||
|
version: 3.0.6
|
||||||
|
resolution: "dompurify@npm:3.0.6"
|
||||||
|
checksum: 10c0/defc5126e1724bbe5dd5835f0de838c6dc9726a73fc74893e4c661a3c1bd5c65189295013afee74ae7097b3be93499539ff9ec66118d3aa46e788266b1f7514c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"domutils@npm:^1.7.0":
|
"domutils@npm:^1.7.0":
|
||||||
version: 1.7.0
|
version: 1.7.0
|
||||||
resolution: "domutils@npm:1.7.0"
|
resolution: "domutils@npm:1.7.0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user