diff --git a/src/ffi.js b/src/ffi.js index dfa1484b5..ab38937e8 100644 --- a/src/ffi.js +++ b/src/ffi.js @@ -32,6 +32,8 @@ Sk.ffi = { const OBJECT_PROTO = Object.prototype; const FUNC_PROTO = Function.prototype; +const MAP_PROTO = Map.prototype; +const SET_PROTO = Set.prototype; /** * maps from Javascript Object/Array/string to Python dict/list/str. @@ -308,6 +310,11 @@ function toPyDict(obj, hooks) { return ret; } +function isCrossOriginWindow(obj) { + // based on https://github.com/weizman/is-cross-origin + return obj !== null && typeof obj === "object" && obj.window === obj && Object.getPrototypeOf(obj) === null; +} + // cache the proxied objects in a weakmap const _proxied = new WeakMap(); const methodSelfCache = new WeakMap(); @@ -338,10 +345,10 @@ function proxy(obj, flags) { } else if (Array.isArray(obj)) { rv = new JsProxyList(obj); } else { - const constructor = obj.constructor; - if (constructor === Map) { + const proto = Object.getPrototypeOf(obj); + if (proto === MAP_PROTO) { rv = new JsProxyMap(obj); - } else if (constructor === Set) { + } else if (proto === SET_PROTO) { rv = new JsProxySet(obj); } else { rv = new JsProxy(obj, flags); @@ -667,6 +674,10 @@ const JsProxy = Sk.abstr.buildNativeClass("Proxy", { const jsName = pyName.toString(); const attr = this.js$wrapped[jsName]; if (attr !== undefined) { + if (isCrossOriginWindow(attr)) { + // we can't do the usual toPy dance, since accessing attributes breaks the same-origin policy + return proxy(attr, { name: "CrossOriginWindow" }); + } // here we override the funcHook to pass the bound object return toPy(attr, boundHook(this.js$wrapped, jsName)); } else if (jsName in this.js$wrapped) { diff --git a/test/unit3/test_skulpt_interop.py b/test/unit3/test_skulpt_interop.py index a7985deeb..2dce7b27b 100644 --- a/test/unit3/test_skulpt_interop.py +++ b/test/unit3/test_skulpt_interop.py @@ -89,6 +89,32 @@ def bar(self): window.method_2 = method_2 self.assertIsNot(method_1, method_2) self.assertIs(window.method_1, window.method_2) + + def test_cross_origin_window(self): + window.x = {"foo": None} + self.assertIsNone(window.x.foo) + + if "window" not in window: + # can't test this in a node environment + return + + iframe = window.document.createElement("iframe") + iframe.src = "http://skulpt.org" + + window.document.body.prepend(iframe) + + contentWindow = iframe.contentWindow + def wait_for_load(resolve, reject): + iframe.onload = resolve + iframe.onerror = reject + + p = window.Promise(wait_for_load).then(lambda *args: print("iframe loaded")) + + with self.assertRaisesRegex(Exception, "SecurityError"): + # should be a cross origin error + hasattr(contentWindow, "foo") + + contentWindow.postMessage({"data": "*"}) if __name__ == "__main__":