mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 15:23:29 -05:00
Make HTML sanitization configurable.
And default to sanitizing setHTML content as well.
This commit is contained in:
parent
65138a68ea
commit
9fac7ffdb6
4 changed files with 76 additions and 30 deletions
10
README.md
10
README.md
|
@ -48,9 +48,15 @@ You can have multiple squire instances in a single page without issue. If you ar
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
Malicious HTML can be a source of XSS and other security issues. I highly recommended you use [DOMPurify](https://github.com/cure53/DOMPurify) with Squire to prevent these security issues. If DOMPurify is included in the page (with the standard global variable), Squire will automatically sanitise any HTML pasted into the editor. (In more detail, it sanitises the HTML given to it in an `insertHTML` call, which is how pastes are inserted).
|
Malicious HTML can be a source of XSS and other security issues. I highly recommended you use [DOMPurify](https://github.com/cure53/DOMPurify) with Squire to prevent these security issues. If DOMPurify is included in the page (with the standard global variable), Squire will automatically sanitise any HTML passed in via `setHTML` or `insertHTML` (which includes HTML the user pastes from the clipboard).
|
||||||
|
|
||||||
Please note though, it **does not automatically sanitise HTML passed in calls to `setHTML`**. Since this is only called from code integrating with Squire, and not user actions, sanitising the HTML here is left to the integration. This is because the default DOMPurify settings might not be the best fit for your application.
|
You can override this by setting properties on the config object (the second argument passed to the constructor, see below). The properties are:
|
||||||
|
|
||||||
|
* **isSetHTMLSanitized**: `Boolean`
|
||||||
|
Should the HTML passed via calls to `setHTML` be passed to the sanitizer? If your app always sanitizes the HTML in some other way before calling this, you may wish to set this to `false` to avoid the overhead.
|
||||||
|
* **isInsertedHTMLSanitized**: `Boolean` (defaults to `true`) – Should the HTML passed via calls to `insertHTML` be passed to the sanitizer? This includes when the user pastes from the clipboard. Since you cannot control what other apps put on the clipboard, it is highly recommended you do not set this to `false`.
|
||||||
|
* **sanitizeToDOMFragment**: `(html: String, isPaste: Boolean) -> DOMFragment`
|
||||||
|
A custom sanitization function. This will be called instead of the default call to DOMPurify to sanitize the potentially dangerous HTML. It is passed two arguments: the first is the string of HTML, the second is a boolean indicating if this content has come from the clipboard, rather than an explicit call by your own code. It must return a DOM Fragment node belonging to the same document as the editor's root node, with the contents being clean DOM nodes to set/insert.
|
||||||
|
|
||||||
Advanced usage
|
Advanced usage
|
||||||
--------------
|
--------------
|
||||||
|
|
|
@ -2556,6 +2556,15 @@ function Squire ( root, config ) {
|
||||||
|
|
||||||
var proto = Squire.prototype;
|
var proto = Squire.prototype;
|
||||||
|
|
||||||
|
var sanitizeToDOMFragment = function ( html/*, isPaste*/ ) {
|
||||||
|
var frag = DOMPurify.sanitize( html, {
|
||||||
|
WHOLE_DOCUMENT: false,
|
||||||
|
RETURN_DOM: true,
|
||||||
|
RETURN_DOM_FRAGMENT: true
|
||||||
|
});
|
||||||
|
return doc.importNode( frag, true );
|
||||||
|
};
|
||||||
|
|
||||||
proto.setConfig = function ( config ) {
|
proto.setConfig = function ( config ) {
|
||||||
config = mergeObjects({
|
config = mergeObjects({
|
||||||
blockTag: 'DIV',
|
blockTag: 'DIV',
|
||||||
|
@ -2571,7 +2580,13 @@ proto.setConfig = function ( config ) {
|
||||||
undo: {
|
undo: {
|
||||||
documentSizeThreshold: -1, // -1 means no threshold
|
documentSizeThreshold: -1, // -1 means no threshold
|
||||||
undoLimit: -1 // -1 means no limit
|
undoLimit: -1 // -1 means no limit
|
||||||
}
|
},
|
||||||
|
isInsertedHTMLSanitized: true,
|
||||||
|
isSetHTMLSanitized: true,
|
||||||
|
sanitizeToDOMFragment:
|
||||||
|
typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ?
|
||||||
|
sanitizeToDOMFragment : null
|
||||||
|
|
||||||
}, config, true );
|
}, config, true );
|
||||||
|
|
||||||
// Users may specify block tag in lower case
|
// Users may specify block tag in lower case
|
||||||
|
@ -3967,14 +3982,21 @@ proto.getHTML = function ( withBookMark ) {
|
||||||
};
|
};
|
||||||
|
|
||||||
proto.setHTML = function ( html ) {
|
proto.setHTML = function ( html ) {
|
||||||
var frag = this._doc.createDocumentFragment();
|
var config = this._config;
|
||||||
var div = this.createElement( 'DIV' );
|
var sanitizeToDOMFragment = config.isSetHTMLSanitized ?
|
||||||
|
config.sanitizeToDOMFragment : null;
|
||||||
var root = this._root;
|
var root = this._root;
|
||||||
var child;
|
var div, frag, child;
|
||||||
|
|
||||||
// Parse HTML into DOM tree
|
// Parse HTML into DOM tree
|
||||||
|
if ( typeof sanitizeToDOMFragment === 'function' ) {
|
||||||
|
frag = sanitizeToDOMFragment( html, false );
|
||||||
|
} else {
|
||||||
|
div = this.createElement( 'DIV' );
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
|
frag = this._doc.createDocumentFragment();
|
||||||
frag.appendChild( empty( div ) );
|
frag.appendChild( empty( div ) );
|
||||||
|
}
|
||||||
|
|
||||||
cleanTree( frag );
|
cleanTree( frag );
|
||||||
cleanupBRs( frag, root );
|
cleanupBRs( frag, root );
|
||||||
|
@ -4109,6 +4131,9 @@ var addLinks = function ( frag, root, self ) {
|
||||||
// insertTreeFragmentIntoRange will delete the selection so that it is replaced
|
// insertTreeFragmentIntoRange will delete the selection so that it is replaced
|
||||||
// by the html being inserted.
|
// by the html being inserted.
|
||||||
proto.insertHTML = function ( html, isPaste ) {
|
proto.insertHTML = function ( html, isPaste ) {
|
||||||
|
var config = this._config;
|
||||||
|
var sanitizeToDOMFragment = config.isInsertedHTMLSanitized ?
|
||||||
|
config.sanitizeToDOMFragment : null;
|
||||||
var range = this.getSelection();
|
var range = this.getSelection();
|
||||||
var doc = this._doc;
|
var doc = this._doc;
|
||||||
var startFragmentIndex, endFragmentIndex;
|
var startFragmentIndex, endFragmentIndex;
|
||||||
|
@ -4117,13 +4142,8 @@ proto.insertHTML = function ( html, isPaste ) {
|
||||||
// Edge doesn't just copy the fragment, but includes the surrounding guff
|
// Edge doesn't just copy the fragment, but includes the surrounding guff
|
||||||
// including the full <head> of the page. Need to strip this out. If
|
// including the full <head> of the page. Need to strip this out. If
|
||||||
// available use DOMPurify to parse and sanitise.
|
// available use DOMPurify to parse and sanitise.
|
||||||
if ( typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ) {
|
if ( typeof sanitizeToDOMFragment === 'function' ) {
|
||||||
frag = DOMPurify.sanitize( html, {
|
frag = sanitizeToDOMFragment( html, isPaste );
|
||||||
WHOLE_DOCUMENT: false,
|
|
||||||
RETURN_DOM: true,
|
|
||||||
RETURN_DOM_FRAGMENT: true
|
|
||||||
});
|
|
||||||
frag = doc.importNode( frag, true );
|
|
||||||
} else {
|
} else {
|
||||||
if ( isPaste ) {
|
if ( isPaste ) {
|
||||||
startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
|
startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -163,6 +163,15 @@ function Squire ( root, config ) {
|
||||||
|
|
||||||
var proto = Squire.prototype;
|
var proto = Squire.prototype;
|
||||||
|
|
||||||
|
var sanitizeToDOMFragment = function ( html/*, isPaste*/ ) {
|
||||||
|
var frag = DOMPurify.sanitize( html, {
|
||||||
|
WHOLE_DOCUMENT: false,
|
||||||
|
RETURN_DOM: true,
|
||||||
|
RETURN_DOM_FRAGMENT: true
|
||||||
|
});
|
||||||
|
return doc.importNode( frag, true );
|
||||||
|
};
|
||||||
|
|
||||||
proto.setConfig = function ( config ) {
|
proto.setConfig = function ( config ) {
|
||||||
config = mergeObjects({
|
config = mergeObjects({
|
||||||
blockTag: 'DIV',
|
blockTag: 'DIV',
|
||||||
|
@ -178,7 +187,13 @@ proto.setConfig = function ( config ) {
|
||||||
undo: {
|
undo: {
|
||||||
documentSizeThreshold: -1, // -1 means no threshold
|
documentSizeThreshold: -1, // -1 means no threshold
|
||||||
undoLimit: -1 // -1 means no limit
|
undoLimit: -1 // -1 means no limit
|
||||||
}
|
},
|
||||||
|
isInsertedHTMLSanitized: true,
|
||||||
|
isSetHTMLSanitized: true,
|
||||||
|
sanitizeToDOMFragment:
|
||||||
|
typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ?
|
||||||
|
sanitizeToDOMFragment : null
|
||||||
|
|
||||||
}, config, true );
|
}, config, true );
|
||||||
|
|
||||||
// Users may specify block tag in lower case
|
// Users may specify block tag in lower case
|
||||||
|
@ -1574,14 +1589,21 @@ proto.getHTML = function ( withBookMark ) {
|
||||||
};
|
};
|
||||||
|
|
||||||
proto.setHTML = function ( html ) {
|
proto.setHTML = function ( html ) {
|
||||||
var frag = this._doc.createDocumentFragment();
|
var config = this._config;
|
||||||
var div = this.createElement( 'DIV' );
|
var sanitizeToDOMFragment = config.isSetHTMLSanitized ?
|
||||||
|
config.sanitizeToDOMFragment : null;
|
||||||
var root = this._root;
|
var root = this._root;
|
||||||
var child;
|
var div, frag, child;
|
||||||
|
|
||||||
// Parse HTML into DOM tree
|
// Parse HTML into DOM tree
|
||||||
|
if ( typeof sanitizeToDOMFragment === 'function' ) {
|
||||||
|
frag = sanitizeToDOMFragment( html, false );
|
||||||
|
} else {
|
||||||
|
div = this.createElement( 'DIV' );
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
|
frag = this._doc.createDocumentFragment();
|
||||||
frag.appendChild( empty( div ) );
|
frag.appendChild( empty( div ) );
|
||||||
|
}
|
||||||
|
|
||||||
cleanTree( frag );
|
cleanTree( frag );
|
||||||
cleanupBRs( frag, root );
|
cleanupBRs( frag, root );
|
||||||
|
@ -1716,6 +1738,9 @@ var addLinks = function ( frag, root, self ) {
|
||||||
// insertTreeFragmentIntoRange will delete the selection so that it is replaced
|
// insertTreeFragmentIntoRange will delete the selection so that it is replaced
|
||||||
// by the html being inserted.
|
// by the html being inserted.
|
||||||
proto.insertHTML = function ( html, isPaste ) {
|
proto.insertHTML = function ( html, isPaste ) {
|
||||||
|
var config = this._config;
|
||||||
|
var sanitizeToDOMFragment = config.isInsertedHTMLSanitized ?
|
||||||
|
config.sanitizeToDOMFragment : null;
|
||||||
var range = this.getSelection();
|
var range = this.getSelection();
|
||||||
var doc = this._doc;
|
var doc = this._doc;
|
||||||
var startFragmentIndex, endFragmentIndex;
|
var startFragmentIndex, endFragmentIndex;
|
||||||
|
@ -1724,13 +1749,8 @@ proto.insertHTML = function ( html, isPaste ) {
|
||||||
// Edge doesn't just copy the fragment, but includes the surrounding guff
|
// Edge doesn't just copy the fragment, but includes the surrounding guff
|
||||||
// including the full <head> of the page. Need to strip this out. If
|
// including the full <head> of the page. Need to strip this out. If
|
||||||
// available use DOMPurify to parse and sanitise.
|
// available use DOMPurify to parse and sanitise.
|
||||||
if ( typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ) {
|
if ( typeof sanitizeToDOMFragment === 'function' ) {
|
||||||
frag = DOMPurify.sanitize( html, {
|
frag = sanitizeToDOMFragment( html, isPaste );
|
||||||
WHOLE_DOCUMENT: false,
|
|
||||||
RETURN_DOM: true,
|
|
||||||
RETURN_DOM_FRAGMENT: true
|
|
||||||
});
|
|
||||||
frag = doc.importNode( frag, true );
|
|
||||||
} else {
|
} else {
|
||||||
if ( isPaste ) {
|
if ( isPaste ) {
|
||||||
startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
|
startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
|
||||||
|
|
Loading…
Reference in a new issue