Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add content security policy support for trustedType innerHTML #4400

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/quill/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"main": "quill.js",
"type": "module",
"dependencies": {
"dompurify": "^3.1.6",
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0",
Expand All @@ -18,6 +19,7 @@
"@babel/preset-env": "^7.24.0",
"@babel/preset-typescript": "^7.23.3",
"@playwright/test": "1.44.1",
"@types/dompurify": "^3.0.5",
"@types/highlight.js": "^9.12.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.10.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/quill/src/core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { ThemeConstructor } from './theme.js';
import scrollRectIntoView from './utils/scrollRectIntoView.js';
import type { Rect } from './utils/scrollRectIntoView.js';
import createRegistryWithFormats from './utils/createRegistryWithFormats.js';
import createTrustedHtml from './utils/createTrustedHtml.js';

const debug = logger('quill');

Expand Down Expand Up @@ -204,7 +205,7 @@ class Quill {
}
const html = this.container.innerHTML.trim();
this.container.classList.add('ql-container');
this.container.innerHTML = '';
this.container.innerHTML = createTrustedHtml('');
instances.set(this.container, this);
this.root = this.addContainer('ql-editor');
this.root.classList.add('ql-blank');
Expand Down
9 changes: 9 additions & 0 deletions packages/quill/src/core/utils/createTrustedHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import DOMPurify from 'dompurify';

const createTrustedHtml = (html: string): string => {
return DOMPurify.sanitize(html, {
RETURN_TRUSTED_TYPE: true,
}) as unknown as string;
};

export default createTrustedHtml;
6 changes: 5 additions & 1 deletion packages/quill/src/modules/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { FontStyle } from '../formats/font.js';
import { SizeStyle } from '../formats/size.js';
import { deleteRange } from './keyboard.js';
import normalizeExternalHTML from './normalizeExternalHTML/index.js';
import createTrustedHtml from "../core/utils/createTrustedHtml";

const debug = logger('quill:clipboard');

Expand Down Expand Up @@ -125,7 +126,10 @@ class Clipboard extends Module<ClipboardOptions> {
}

protected convertHTML(html: string) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const doc = new DOMParser().parseFromString(
createTrustedHtml(html),
'text/html',
);
this.normalizeHTML(doc);
const container = doc.body;
const nodeMatches = new WeakMap();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import createTrustedHtml from '../../../core/utils/createTrustedHtml.js';

const ignoreRegexp = /\bmso-list:[^;]*ignore/i;
const idRegexp = /\bmso-list:[^;]*\bl(\d+)/i;
const indentRegexp = /\bmso-list:[^;]*\blevel(\d+)/i;
Expand Down Expand Up @@ -72,7 +74,7 @@ const normalizeListItem = (doc: Document) => {
if (listItem.indent > 1) {
li.setAttribute('class', `ql-indent-${listItem.indent - 1}`);
}
li.innerHTML = listItem.element.innerHTML;
li.innerHTML = createTrustedHtml(listItem.element.innerHTML);
ul.appendChild(li);
});

Expand Down
7 changes: 5 additions & 2 deletions packages/quill/src/modules/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CursorBlot from '../blots/cursor.js';
import TextBlot, { escapeText } from '../blots/text.js';
import CodeBlock, { CodeBlockContainer } from '../formats/code.js';
import { traverse } from './clipboard.js';
import createTrustedHtml from '../core/utils/createTrustedHtml.js';

const TokenAttributor = new ClassAttributor('code-token', 'hljs', {
scope: Scope.INLINE,
Expand Down Expand Up @@ -191,7 +192,7 @@ interface SyntaxOptions {
hljs: any;
}

const highlight = (lib: any, language: string, text: string) => {
const highlight = (lib: any, language: string, text: string): string => {
if (typeof lib.versionString === 'string') {
const majorVersion = lib.versionString.split('.')[0];
if (parseInt(majorVersion, 10) >= 11) {
Expand Down Expand Up @@ -301,7 +302,9 @@ class Syntax extends Module<SyntaxOptions> {
}
const container = this.quill.root.ownerDocument.createElement('div');
container.classList.add(CodeBlock.className);
container.innerHTML = highlight(this.options.hljs, language, text);
container.innerHTML = createTrustedHtml(
highlight(this.options.hljs, language, text),
);
return traverse(
this.quill.scroll,
container,
Expand Down
11 changes: 7 additions & 4 deletions packages/quill/src/themes/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type History from '../modules/history.js';
import type Keyboard from '../modules/keyboard.js';
import type Uploader from '../modules/uploader.js';
import type Selection from '../core/selection.js';
import createTrustedHtml from '../core/utils/createTrustedHtml.js';

const ALIGNS = [false, 'center', 'right', 'justify'];

Expand Down Expand Up @@ -119,18 +120,20 @@ class BaseTheme extends Theme {
name = name.slice('ql-'.length);
if (icons[name] == null) return;
if (name === 'direction') {
// @ts-expect-error
button.innerHTML = icons[name][''] + icons[name].rtl;
button.innerHTML = createTrustedHtml(
// @ts-expect-error
icons[name][''] + icons[name].rtl,
);
} else if (typeof icons[name] === 'string') {
// @ts-expect-error
button.innerHTML = icons[name];
button.innerHTML = createTrustedHtml(icons[name]);
} else {
// @ts-expect-error
const value = button.value || '';
// @ts-expect-error
if (value != null && icons[name][value]) {
// @ts-expect-error
button.innerHTML = icons[name][value];
button.innerHTML = createTrustedHtml(icons[name][value]);
}
}
});
Expand Down
3 changes: 2 additions & 1 deletion packages/quill/src/ui/color-picker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Picker from './picker.js';
import createTrustedHtml from '../core/utils/createTrustedHtml.js';

class ColorPicker extends Picker {
constructor(select: HTMLSelectElement, label: string) {
super(select);
this.label.innerHTML = label;
this.label.innerHTML = createTrustedHtml(label);
this.container.classList.add('ql-color-picker');
Array.from(this.container.querySelectorAll('.ql-picker-item'))
.slice(0, 7)
Expand Down
7 changes: 5 additions & 2 deletions packages/quill/src/ui/icon-picker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Picker from './picker.js';
import createTrustedHtml from '../core/utils/createTrustedHtml.js';

class IconPicker extends Picker {
defaultItem: HTMLElement | null;
Expand All @@ -8,7 +9,9 @@ class IconPicker extends Picker {
this.container.classList.add('ql-icon-picker');
Array.from(this.container.querySelectorAll('.ql-picker-item')).forEach(
(item) => {
item.innerHTML = icons[item.getAttribute('data-value') || ''];
item.innerHTML = createTrustedHtml(
icons[item.getAttribute('data-value') || ''],
);
},
);
this.defaultItem = this.container.querySelector('.ql-selected');
Expand All @@ -20,7 +23,7 @@ class IconPicker extends Picker {
const item = target || this.defaultItem;
if (item != null) {
if (this.label.innerHTML === item.innerHTML) return;
this.label.innerHTML = item.innerHTML;
this.label.innerHTML = createTrustedHtml(item.innerHTML);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/quill/src/ui/picker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import DropdownIcon from '../assets/icons/dropdown.svg';
import createTrustedHtml from '../core/utils/createTrustedHtml.js';

let optionsCounter = 0;

Expand Down Expand Up @@ -84,7 +85,7 @@ class Picker {
buildLabel() {
const label = document.createElement('span');
label.classList.add('ql-picker-label');
label.innerHTML = DropdownIcon;
label.innerHTML = createTrustedHtml(DropdownIcon);
// @ts-expect-error
label.tabIndex = '0';
label.setAttribute('role', 'button');
Expand Down
3 changes: 2 additions & 1 deletion packages/quill/src/ui/tooltip.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type Quill from '../core.js';
import type { Bounds } from '../core/selection.js';
import createTrustedHtml from '../core/utils/createTrustedHtml.js';

const isScrollable = (el: Element) => {
const { overflowY } = getComputedStyle(el, null);
Expand All @@ -16,7 +17,7 @@ class Tooltip {
this.boundsContainer = boundsContainer || document.body;
this.root = quill.addContainer('ql-tooltip');
// @ts-expect-error
this.root.innerHTML = this.constructor.TEMPLATE;
this.root.innerHTML = createTrustedHtml(this.constructor.TEMPLATE);
if (isScrollable(this.quill.root)) {
this.quill.root.addEventListener('scroll', () => {
this.root.style.marginTop = `${-1 * this.quill.root.scrollTop}px`;
Expand Down