2 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
4 * Browser Compatibility:
5 * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
6 * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
8 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
9 * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
10 * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
11 * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
12 * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
13 * can do anything as if the sandbox attribute wasn't set
15 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
16 * @param {Object} [config] Optional parameters
19 * new wysihtml5.dom.Sandbox(function(sandbox) {
20 * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
23 (function(wysihtml5) {
25 * Default configuration
29 * Properties to unset/protect on the window object
32 "parent", "top", "opener", "frameElement", "frames",
33 "localStorage", "globalStorage", "sessionStorage", "indexedDB"
36 * Properties on the window object which are set to an empty function
39 "open", "close", "openDialog", "showModalDialog",
40 "alert", "confirm", "prompt",
41 "openDatabase", "postMessage",
42 "XMLHttpRequest", "XDomainRequest"
45 * Properties to unset/protect on the document object
47 documentProperties = [
49 "write", "open", "close"
52 wysihtml5.dom.Sandbox = Base.extend(
53 /** @scope wysihtml5.dom.Sandbox.prototype */ {
55 constructor: function(readyCallback, config) {
56 this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
57 this.config = wysihtml5.lang.object({}).merge(config).get();
58 this.editableArea = this._createIframe();
61 insertInto: function(element) {
62 if (typeof(element) === "string") {
63 element = doc.getElementById(element);
66 element.appendChild(this.editableArea);
69 getIframe: function() {
70 return this.editableArea;
73 getWindow: function() {
77 getDocument: function() {
82 var iframe = this.getIframe();
83 iframe.parentNode.removeChild(iframe);
86 _readyError: function() {
87 throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
91 * Creates the sandbox iframe
93 * Some important notes:
94 * - We can't use HTML5 sandbox for now:
95 * setting it causes that the iframe's dom can't be accessed from the outside
96 * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
97 * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
98 * In order to make this happen we need to set the "allow-scripts" flag.
99 * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
100 * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
101 * - IE needs to have the security="restricted" attribute set before the iframe is
102 * inserted into the dom tree
103 * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
104 * though it supports it
105 * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
106 * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
107 * on the onreadystatechange event
109 _createIframe: function() {
111 iframe = doc.createElement("iframe");
112 iframe.className = "wysihtml5-sandbox";
113 wysihtml5.dom.setAttributes({
114 "security": "restricted",
115 "allowtransparency": "true",
123 // Setting the src like this prevents ssl warnings in IE6
124 if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
125 iframe.src = "javascript:'<html></html>'";
128 iframe.onload = function() {
129 iframe.onreadystatechange = iframe.onload = null;
130 that._onLoadIframe(iframe);
133 iframe.onreadystatechange = function() {
134 if (/loaded|complete/.test(iframe.readyState)) {
135 iframe.onreadystatechange = iframe.onload = null;
136 that._onLoadIframe(iframe);
144 * Callback for when the iframe has finished loading
146 _onLoadIframe: function(iframe) {
147 // don't resume when the iframe got unloaded (eg. by removing it from the dom)
148 if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
153 iframeWindow = iframe.contentWindow,
154 iframeDocument = iframe.contentWindow.document,
155 charset = doc.characterSet || doc.charset || "utf-8",
156 sandboxHtml = this._getHtml({
158 stylesheets: this.config.stylesheets
161 // Create the basic dom tree including proper DOCTYPE and charset
162 iframeDocument.open("text/html", "replace");
163 iframeDocument.write(sandboxHtml);
164 iframeDocument.close();
166 this.getWindow = function() { return iframe.contentWindow; };
167 this.getDocument = function() { return iframe.contentWindow.document; };
169 // Catch js errors and pass them to the parent's onerror event
170 // addEventListener("error") doesn't work properly in some browsers
171 // TODO: apparently this doesn't work in IE9!
172 iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
173 throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
176 if (!wysihtml5.browser.supportsSandboxedIframes()) {
177 // Unset a bunch of sensitive variables
178 // Please note: This isn't hack safe!
179 // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
180 // IE is secure though, which is the most important thing, since IE is the only browser, who
181 // takes over scripts & styles into contentEditable elements when copied from external websites
182 // or applications (Microsoft Word, ...)
184 for (i=0, length=windowProperties.length; i<length; i++) {
185 this._unset(iframeWindow, windowProperties[i]);
187 for (i=0, length=windowProperties2.length; i<length; i++) {
188 this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
190 for (i=0, length=documentProperties.length; i<length; i++) {
191 this._unset(iframeDocument, documentProperties[i]);
193 // This doesn't work in Safari 5
194 // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
195 this._unset(iframeDocument, "cookie", "", true);
200 // Trigger the callback
201 setTimeout(function() { that.callback(that); }, 0);
204 _getHtml: function(templateVars) {
205 var stylesheets = templateVars.stylesheets,
209 stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
211 length = stylesheets.length;
212 for (; i<length; i++) {
213 html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
216 templateVars.stylesheets = html;
218 return wysihtml5.lang.string(
219 '<!DOCTYPE html><html><head>'
220 + '<meta charset="#{charset}">#{stylesheets}</head>'
221 + '<body></body></html>'
222 ).interpolate(templateVars);
226 * Method to unset/override existing variables
228 * // Make cookie unreadable and unwritable
229 * this._unset(document, "cookie", "", true);
231 _unset: function(object, property, value, setter) {
232 try { object[property] = value; } catch(e) {}
234 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
236 try { object.__defineSetter__(property, function() {}); } catch(e) {}
239 if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
242 get: function() { return value; }
245 config.set = function() {};
247 Object.defineProperty(object, property, config);