Skip to content

Commit

Permalink
Sanitize rich-text pasted into ckeditor (indico#5571)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasr8 authored Nov 25, 2022
1 parent a4a0949 commit 26b60a3
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Improvements
- Warn when a user cannot create an event in the current category (:pr:`5572`)
- Display all contributions in 'My contributions' and not just those with
submitter privileges (:pr:`5575`)
- Apply stronger sanitization on rich-text content pasted into CKEditor
(:issue:`5560`, :pr:`5571`)

Bugfixes
^^^^^^^^
Expand Down
21 changes: 21 additions & 0 deletions indico/web/client/js/ckeditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// modify it under the terms of the MIT License; see the
// LICENSE file for more details.

import {sanitizeHtml} from './utils/sanitize';

export const getConfig = ({
images = true,
imageUploadURL = null,
Expand Down Expand Up @@ -184,3 +186,22 @@ export const getConfig = ({
}
: undefined,
});

// Sanitize HTML pasted into ckeditor.
// Use it with the clipboardInput event and pass the editor instance:
//
// editor.editing.view.document.on('clipboardInput', sanitizeHtmlOnPaste(editor))
//
// More info: https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/clipboard.html
export function sanitizeHtmlOnPaste(editor) {
return (ev, data) => {
if (data.method !== 'paste' || data.content) {
return;
}
const dataTransfer = data.dataTransfer;
const contentData = dataTransfer.getData('text/html');
if (contentData) {
data.content = editor.data.htmlProcessor.toView(sanitizeHtml(contentData));
}
};
}
6 changes: 5 additions & 1 deletion indico/web/client/js/jquery/widgets/jinja/ckeditor_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import ClassicEditor from 'ckeditor';
import _ from 'lodash';

import {getConfig} from 'indico/ckeditor';
import {getConfig, sanitizeHtmlOnPaste} from 'indico/ckeditor';

(function(global) {
global.setupCKEditorWidget = async function setupCKEditorWidget(options) {
Expand All @@ -26,6 +26,10 @@ import {getConfig} from 'indico/ckeditor';
field.dispatchEvent(new Event('change', {bubbles: true}));
}, 250)
);
// Sanitize pasted HTML
editor.editing.view.document.on('clipboardInput', sanitizeHtmlOnPaste(editor), {
priority: 'normal',
});
editor.editing.view.change(writer => {
writer.setStyle(
'width',
Expand Down
6 changes: 5 additions & 1 deletion indico/web/client/js/react/components/TextEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import React, {useMemo, useState} from 'react';
import {Field} from 'react-final-form';
import {Dimmer, Loader} from 'semantic-ui-react';

import {getConfig} from 'indico/ckeditor';
import {getConfig, sanitizeHtmlOnPaste} from 'indico/ckeditor';
import {Translate} from 'indico/react/i18n';

import {FinalField} from '../forms';
Expand Down Expand Up @@ -41,6 +41,10 @@ export default function TextEditor({
writer.setStyle('width', width, editor.editing.view.document.getRoot());
writer.setStyle('height', height, editor.editing.view.document.getRoot());
});
// Sanitize pasted HTML
editor.editing.view.document.on('clipboardInput', sanitizeHtmlOnPaste(editor), {
priority: 'normal',
});
if (setValidationError) {
editor.plugins._plugins.get('SourceEditing').on('change:isSourceEditingMode', evt => {
if (evt.source.isSourceEditingMode) {
Expand Down
70 changes: 70 additions & 0 deletions indico/web/client/js/utils/sanitize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// This file is part of Indico.
// Copyright (C) 2002 - 2022 CERN
//
// Indico is free software; you can redistribute it and/or
// modify it under the terms of the MIT License; see the
// LICENSE file for more details.

import _sanitizeHtml from 'sanitize-html';

/* eslint array-element-newline: off */

// The following are whitelisted tags, attributes & CSS styles for
// sanitizing HTML provided by the user, such as when pasting into ckeditor.
// It is the same configuration that is used by bleach on the server-side (except for the legacy attributes).
// See 'indico/util/string.py'

// prettier-ignore
const ALLOWED_TAGS = [
// bleach.ALLOWED_TAGS
'a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'strong', 'ul',
// BLEACH_ALLOWED_TAGS
'sup', 'sub', 'small', 'br', 'p', 'table', 'thead', 'tbody', 'th', 'tr', 'td', 'img', 'hr', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'pre', 'dl', 'dd', 'dt', 'figure', 'blockquote',
// BLEACH_ALLOWED_TAGS_HTML
'address', 'area', 'bdo', 'big', 'caption', 'center', 'cite', 'col', 'colgroup', 'del', 'dfn', 'dir', 'div',
'fieldset', 'font', 'ins', 'kbd', 'legend', 'map', 'menu', 'q', 's', 'samp', 'span', 'strike', 'tfoot', 'tt', 'u',
'var'
];

const ALLOWED_ATTRIBUTES = {
'*': ['style'],
// bleach.ALLOWED_ATTRIBUTES
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title'],
// BLEACH_ALLOWED_ATTRIBUTES
'img': ['src', 'alt', 'style'],
};

// prettier-ignore
const ALLOWED_STYLES = [
'background-color', 'border-top-color', 'border-top-style', 'border-top-width', 'border-top', 'border-right-color',
'border-right-style', 'border-right-width', 'border-right', 'border-bottom-color', 'border-bottom-style',
'border-bottom-width', 'border-bottom', 'border-left-color', 'border-left-style', 'border-left-width',
'border-left', 'border-color', 'border-style', 'border-width', 'border', 'bottom', 'border-collapse',
'border-spacing', 'color', 'clear', 'clip', 'caption-side', 'display', 'direction', 'empty-cells', 'float',
'font-size', 'font-family', 'font-style', 'font', 'font-variant', 'font-weight', 'font-size-adjust', 'font-stretch',
'height', 'left', 'list-style-type', 'list-style-position', 'line-height', 'letter-spacing', 'marker-offset',
'margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'max-height', 'min-height', 'max-width',
'min-width', 'marks', 'overflow', 'outline-color', 'outline-style', 'outline-width', 'outline', 'orphans',
'position', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'padding', 'page', 'page-break-after',
'page-break-before', 'page-break-inside', 'quotes', 'right', 'size', 'text-align', 'top', 'table-layout',
'text-decoration', 'text-indent', 'text-shadow', 'text-transform', 'unicode-bidi', 'visibility', 'vertical-align',
'width', 'widows', 'white-space', 'word-spacing', 'word-wrap', 'z-index'
]

// Sanitize user-provided HTML, such as when pasting into ckeditor.
export function sanitizeHtml(dirty) {
const matchAny = /^.*$/;
const allowedStyles = ALLOWED_STYLES.reduce((styles, name) => {
styles[name] = [matchAny];
return styles;
}, {});

return _sanitizeHtml(dirty, {
allowedTags: ALLOWED_TAGS,
allowedAttributes: ALLOWED_ATTRIBUTES,
allowedStyles,
});
}
Loading

0 comments on commit 26b60a3

Please sign in to comment.