From 1e2e6529d2bd082e5fc84ff3c9bb634c68c07018 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 22 Feb 2014 04:08:24 +0000 Subject: [PATCH] Critical privacy/data integrity fix: Move cross-domain capable message listener into own extension (ext-xdomain-messaging.js) and do not include by default (the extension now won't work anyways without an allowedOrigins config first being set (in config.js) for security reasons (and not via URL)); add allowedOrigins config and demo use in config-sample.js; JSLint; update embedapi.html to supply the xdomain extension in case running xdomain (again, allowedOrigins must be supplied in the local copy of config.js for this to work); modify embedapi.js to allow reuse of cross-domain API with same-domain usage, but without the intermediate JSON parsing which could lose some non-JSONable arguments or response. git-svn-id: http://svg-edit.googlecode.com/svn/trunk@2714 eee81c28-f429-11dd-99c0-75d572ba1ddd --- editor/config-sample.js | 7 + editor/embedapi.html | 14 +- editor/embedapi.js | 150 +++++++++++++-------- editor/extensions/ext-xdomain-messaging.js | 42 ++++++ editor/svg-editor.js | 65 +++++---- 5 files changed, 181 insertions(+), 97 deletions(-) create mode 100644 editor/extensions/ext-xdomain-messaging.js diff --git a/editor/config-sample.js b/editor/config-sample.js index 39118a2a..fd93a225 100644 --- a/editor/config-sample.js +++ b/editor/config-sample.js @@ -79,6 +79,13 @@ svgEditor.setConfig({ // langPath: 'locale/', // extPath: 'extensions/', // jGraduatePath: 'jgraduate/images/', + /* + Uncomment the following to allow at least same domain (embedded) access, + including file:// access. + Setting as `['*']` would allow any domain to access but would be unsafe to + data privacy and integrity. + */ + // allowedOrigins: [window.location.origin || 'null'], // May be 'null' (as a string) when used as a file:// URL // DOCUMENT PROPERTIES // dimensions: [640, 480], // EDITOR OPTIONS diff --git a/editor/embedapi.html b/editor/embedapi.html index 5c76ced7..32be66d4 100644 --- a/editor/embedapi.html +++ b/editor/embedapi.html @@ -7,11 +7,12 @@
- + diff --git a/editor/embedapi.js b/editor/embedapi.js index 25316a0f..bac6ece1 100644 --- a/editor/embedapi.js +++ b/editor/embedapi.js @@ -6,9 +6,9 @@ General usage: - Initialize the magic with: var svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit); - Pass functions in this format: -svgCanvas.setSvgString("string") +svgCanvas.setSvgString('string') - Or if a callback is needed: -svgCanvas.setSvgString("string")(function(data, error){ +svgCanvas.setSvgString('string')(function(data, error){ if (error){ // There was an error } else{ @@ -22,7 +22,7 @@ and all documentation is unchanged. However, this file depends on the postMessage API which can only support JSON-serializable arguments and return values, so, for example, arguments whose value is -"undefined", a function, a non-finite number, or a built-in +'undefined', a function, a non-finite number, or a built-in object like Date(), RegExp(), etc. will most likely not behave as expected. In such a case one may need to host the SVG editor on the same domain and reference the @@ -32,7 +32,7 @@ The only other difference is when handling returns: the callback notation is used instead. var blah = new EmbeddedSVGEdit(window.frames.svgedit); -blah.clearSelection("woot","blah",1337,[1,2,3,4,5,"moo"],-42,{a: "tree",b:6, c: 9})(function(){console.log("GET DATA",arguments)}) +blah.clearSelection('woot', 'blah', 1337, [1, 2, 3, 4, 5, 'moo'], -42, {a: 'tree',b:6, c: 9})(function(){console.log('GET DATA',arguments)}) */ (function () {'use strict'; @@ -40,7 +40,7 @@ blah.clearSelection("woot","blah",1337,[1,2,3,4,5,"moo"],-42,{a: "tree",b:6, c: var cbid = 0; function getCallbackSetter (d) { - return function(){ + return function () { var t = this, // New callback args = [].slice.call(arguments), cbid = t.send(d, args, function(){}); // The callback (currently it's nothing, but will be set later) @@ -51,8 +51,44 @@ function getCallbackSetter (d) { }; } -function EmbeddedSVGEdit(frame){ - if (!(this instanceof EmbeddedSVGEdit)) { // Allow invocation without "new" keyword +/* +* Having this separate from messageListener allows us to +* avoid using JSON parsing (and its limitations) in the case +* of same domain control +*/ +function addCallback (t, data) { + var result = data.result || data.error; + cbid = data.id; + if (t.callbacks[cbid]) { + if (data.result) { + t.callbacks[cbid](result); + } else { + t.callbacks[cbid](result, 'error'); + } + } +} + +function messageListener (e) { + // We accept and post strings as opposed to objets for the sake of IE9 support; this + // will most likely be changed in the future + if (typeof e.data !== 'string') { + return; + } + var data = e.data && JSON.parse(e.data); + if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit') { + return; + } + addCallback(this, data); +} + +function getMessageListener (t) { + return function (e) { + messageListener.call(t, e); + }; +} + +function EmbeddedSVGEdit (frame) { + if (!(this instanceof EmbeddedSVGEdit)) { // Allow invocation without 'new' keyword return new EmbeddedSVGEdit(frame); } // Initialize communication @@ -61,71 +97,75 @@ function EmbeddedSVGEdit(frame){ // List of functions extracted with this: // Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html - // for (var i=0,q=[],f = document.querySelectorAll("div.CFunction h3.CTitle a"); i < f.length; i++) { q.push(f[i].name); }; q - // var functions = ["clearSelection", "addToSelection", "removeFromSelection", "open", "save", "getSvgString", "setSvgString", - // "createLayer", "deleteCurrentLayer", "setCurrentLayer", "renameCurrentLayer", "setCurrentLayerPosition", "setLayerVisibility", - // "moveSelectedToLayer", "clear"]; + // for (var i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q + // var functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString', + // 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', + // 'moveSelectedToLayer', 'clear']; // Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API - // var l = []; for (var i in svgCanvas){ if (typeof svgCanvas[i] == "function") { l.push(i);} }; + // var l = []; for (var i in svgCanvas){ if (typeof svgCanvas[i] == 'function') { l.push(i);} }; // Run in svgedit itself var i, - t = this, - functions = ["updateElementFromJson", "embedImage", "fixOperaXML", "clearSelection", - "addToSelection", - "removeFromSelection", "addNodeToSelection", "open", "save", "getSvgString", "setSvgString", "createLayer", - "deleteCurrentLayer", "getCurrentDrawing", "setCurrentLayer", "renameCurrentLayer", "setCurrentLayerPosition", - "setLayerVisibility", "moveSelectedToLayer", "clear", "clearPath", "getNodePoint", "clonePathNode", "deletePathNode", - "getResolution", "getImageTitle", "setImageTitle", "setResolution", "setBBoxZoom", "setZoom", "getMode", "setMode", - "getStrokeColor", "setStrokeColor", "getFillColor", "setFillColor", "setStrokePaint", "setFillPaint", "getStrokeWidth", - "setStrokeWidth", "getStrokeStyle", "setStrokeStyle", "getOpacity", "setOpacity", "getFillOpacity", "setFillOpacity", - "getStrokeOpacity", "setStrokeOpacity", "getTransformList", "getBBox", "getRotationAngle", "setRotationAngle", "each", - "bind", "setIdPrefix", "getBold", "setBold", "getItalic", "setItalic", "getFontFamily", "setFontFamily", "getFontSize", - "setFontSize", "getText", "setTextContent", "setImageURL", "setRectRadius", "setSegType", "quickClone", - "changeSelectedAttributeNoUndo", "changeSelectedAttribute", "deleteSelectedElements", "groupSelectedElements", "zoomChanged", - "ungroupSelectedElement", "moveToTopSelectedElement", "moveToBottomSelectedElement", "moveSelectedElements", - "getStrokedBBox", "getVisibleElements", "cycleElement", "getUndoStackSize", "getRedoStackSize", "getNextUndoCommandText", - "getNextRedoCommandText", "undo", "redo", "cloneSelectedElements", "alignSelectedElements", "getZoom", "getVersion", - "setIconSize", "setLang", "setCustomHandlers"]; + functions = ['updateElementFromJson', 'embedImage', 'fixOperaXML', 'clearSelection', + 'addToSelection', + 'removeFromSelection', 'addNodeToSelection', 'open', 'save', 'getSvgString', 'setSvgString', 'createLayer', + 'deleteCurrentLayer', 'getCurrentDrawing', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', + 'setLayerVisibility', 'moveSelectedToLayer', 'clear', 'clearPath', 'getNodePoint', 'clonePathNode', 'deletePathNode', + 'getResolution', 'getImageTitle', 'setImageTitle', 'setResolution', 'setBBoxZoom', 'setZoom', 'getMode', 'setMode', + 'getStrokeColor', 'setStrokeColor', 'getFillColor', 'setFillColor', 'setStrokePaint', 'setFillPaint', 'getStrokeWidth', + 'setStrokeWidth', 'getStrokeStyle', 'setStrokeStyle', 'getOpacity', 'setOpacity', 'getFillOpacity', 'setFillOpacity', + 'getStrokeOpacity', 'setStrokeOpacity', 'getTransformList', 'getBBox', 'getRotationAngle', 'setRotationAngle', 'each', + 'bind', 'setIdPrefix', 'getBold', 'setBold', 'getItalic', 'setItalic', 'getFontFamily', 'setFontFamily', 'getFontSize', + 'setFontSize', 'getText', 'setTextContent', 'setImageURL', 'setRectRadius', 'setSegType', 'quickClone', + 'changeSelectedAttributeNoUndo', 'changeSelectedAttribute', 'deleteSelectedElements', 'groupSelectedElements', 'zoomChanged', + 'ungroupSelectedElement', 'moveToTopSelectedElement', 'moveToBottomSelectedElement', 'moveSelectedElements', + 'getStrokedBBox', 'getVisibleElements', 'cycleElement', 'getUndoStackSize', 'getRedoStackSize', 'getNextUndoCommandText', + 'getNextRedoCommandText', 'undo', 'redo', 'cloneSelectedElements', 'alignSelectedElements', 'getZoom', 'getVersion', + 'setIconSize', 'setLang', 'setCustomHandlers']; // TODO: rewrite the following, it's pretty scary. for (i = 0; i < functions.length; i++) { this[functions[i]] = getCallbackSetter(functions[i]); } - + // Older IE may need a polyfill for addEventListener, but so it would for SVG - window.addEventListener('message', function(e) { - // We accept and post strings as opposed to objets for the sake of IE9 support; this - // will most likely be changed in the future - if (typeof e.data !== 'string') { - return; - } - var result, cbid, - data = e.data && JSON.parse(e.data); - if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit') { - return; - } - result = data.result || data.error; - cbid = data.id; - if (t.callbacks[cbid]) { - if (data.result) { - t.callbacks[cbid](result); - } else { - t.callbacks[cbid](result, "error"); - } - } - }, false); + window.addEventListener('message', getMessageListener(this), false); } -EmbeddedSVGEdit.prototype.send = function(name, args, callback){ +EmbeddedSVGEdit.prototype.send = function (name, args, callback){ var t = this; cbid++; this.callbacks[cbid] = callback; - setTimeout(function(){ // Delay for the callback to be set in case its synchronous - // Todo: Handle non-JSON arguments and return values (undefined, nonfinite numbers, functions, and built-in objects like Date, RegExp), etc.? + setTimeout(function () { // Delay for the callback to be set in case its synchronous + /* + * Todo: Handle non-JSON arguments and return values (undefined, + * nonfinite numbers, functions, and built-in objects like Date, + * RegExp), etc.? Allow promises instead of callbacks? Review + * SVG-Edit functions for whether JSON-able parameters can be + * made compatile with all API functionality + */ // We accept and post strings for the sake of IE9 support - t.frame.contentWindow.postMessage(JSON.stringify({namespace: "svgCanvas", id: cbid, name: name, args: args}), '*'); + if (window.location.origin === t.frame.contentWindow.location.origin) { + // Although we do not really need this API if we are working same + // domain, it could allow us to write in a way that would work + // cross-domain as well, assuming we stick to the argument limitations + // of the current JSON-based communication API (e.g., not passing + // callbacks). We might be able to address these shortcomings; see + // the todo elsewhere in this file. + var message = {id: cbid}, + svgCanvas = t.frame.contentWindow.svgCanvas; + try { + message.result = svgCanvas[name].apply(svgCanvas, args); + } + catch (err) { + message.error = err.message; + } + addCallback(t, message); + } + else { // Requires the ext-xdomain-messaging.js extension + t.frame.contentWindow.postMessage(JSON.stringify({namespace: 'svgCanvas', id: cbid, name: name, args: args}), '*'); + } }, 0); return cbid; }; diff --git a/editor/extensions/ext-xdomain-messaging.js b/editor/extensions/ext-xdomain-messaging.js new file mode 100644 index 00000000..4efcb1a4 --- /dev/null +++ b/editor/extensions/ext-xdomain-messaging.js @@ -0,0 +1,42 @@ +/** +* Should not be needed for same domain control (just call via child frame), +* but an API common for cross-domain and same domain use can be found +* in embedapi.js with a demo at embedapi.html +*/ +/*globals svgEditor, svgCanvas*/ +svgEditor.addExtension('xdomain-messaging', function() {'use strict'; + try { + window.addEventListener('message', function(e) { + // We accept and post strings for the sake of IE9 support + if (typeof e.data !== 'string' || e.data.charAt() === '|') { + return; + } + var cbid, name, args, message, allowedOrigins, data = JSON.parse(e.data); + if (!data || typeof data !== 'object' || data.namespace !== 'svgCanvas') { + return; + } + // The default is not to allow any origins, including even the same domain or if run on a file:// URL + // See config-sample.js for an example of how to configure + allowedOrigins = svgEditor.curConfig.allowedOrigins; + if (allowedOrigins.indexOf('*') === -1 && allowedOrigins.indexOf(e.origin) === -1) { + return; + } + cbid = data.id; + name = data.name; + args = data.args; + message = { + namespace: 'svg-edit', + id: cbid + }; + try { + message.result = svgCanvas[name].apply(svgCanvas, args); + } catch (err) { + message.error = err.message; + } + e.source.postMessage(JSON.stringify(message), '*'); + }, false); + } + catch (err) { + console.log('Error with xdomain message listener: ' + err); + } +}); diff --git a/editor/svg-editor.js b/editor/svg-editor.js index 881936c9..39f377a3 100644 --- a/editor/svg-editor.js +++ b/editor/svg-editor.js @@ -68,7 +68,22 @@ TO-DOS curConfig = { // We do not put on defaultConfig to simplify object copying // procedures (we obtain instead from defaultExtensions) - extensions: [] + extensions: [], + /** + * Can use window.location.origin to indicate the current + * origin. Can contain a '*' to allow all domains or 'null' (as + * a string) to support all file:// URLs. Cannot be set by + * URL for security reasons (not safe, at least for + * privacy or data integrity of SVG content). + * Might have been fairly safe to allow + * `new URL(window.location.href).origin` by default but + * avoiding it ensures some more security that even third + * party apps on the same domain also cannot communicate + * with this app by default. + * For use with ext-xdomain-messaging.js + * @todo We might instead make as a user-facing preference. + */ + allowedOrigins: [] }, defaultExtensions = [ 'ext-overview_window.js', @@ -315,13 +330,17 @@ TO-DOS $.pref(key, val); } } - else if (key === 'extensions') { + else if (['extensions', 'allowedOrigins'].indexOf(key) > -1) { if (cfgCfg.overwrite === false && - (curConfig.preventAllURLConfig || curConfig.lockExtensions) + ( + curConfig.preventAllURLConfig || + key === 'allowedOrigins' || + (key === 'extensions' && curConfig.lockExtensions) + ) ) { return; } - curConfig.extensions = curConfig.extensions.concat(val); // We will handle any dupes later + curConfig[key] = curConfig[key].concat(val); // We will handle any dupes later } // Only allow other curConfig if defined in defaultConfig else if (defaultConfig.hasOwnProperty(key)) { @@ -421,13 +440,15 @@ TO-DOS function setupCurConfig () { curConfig = $.extend(true, {}, defaultConfig, curConfig); // Now safe to merge with priority for curConfig in the event any are already set - // Now deal with extensions + // Now deal with extensions and other array config if (!curConfig.noDefaultExtensions) { curConfig.extensions = curConfig.extensions.concat(defaultExtensions); } // ...and remove any dupes - curConfig.extensions = $.grep(curConfig.extensions, function (n, i) { - return i === curConfig.extensions.indexOf(n); + $.each(['extensions', 'allowedOrigins'], function (i, cfg) { + curConfig[cfg] = $.grep(curConfig[cfg], function (n, i) { + return i === curConfig[cfg].indexOf(n); + }); }); // Export updated config editor.curConfig = curConfig; @@ -4948,33 +4969,9 @@ TO-DOS updateCanvas(true); // }); - // var revnums = "svg-editor.js ($Rev$) "; - // revnums += svgCanvas.getVersion(); - // $('#copyright')[0].setAttribute('title', revnums); - - // Callback handler for embedapi.js - try { - window.addEventListener('message', function(e) { - // We accept and post strings for the sake of IE9 support - if (typeof e.data !== 'string' || e.data.charAt() === '|') { - return; - } - var data = JSON.parse(e.data); - if (!data || typeof data !== 'object' || data.namespace !== 'svgCanvas') { - return; - } - var cbid = data.id, - name = data.name, - args = data.args; - try { - e.source.postMessage(JSON.stringify({namespace: 'svg-edit', id: cbid, result: svgCanvas[name].apply(svgCanvas, args)}), '*'); - } catch(err) { - e.source.postMessage(JSON.stringify({namespace: 'svg-edit', id: cbid, error: err.message}), '*'); - } - }, false); - } catch(err) { - window.embed_error = err; - } + // var revnums = "svg-editor.js ($Rev$) "; + // revnums += svgCanvas.getVersion(); + // $('#copyright')[0].setAttribute('title', revnums); // For Compatibility with older extensions $(function() {