diff --git a/docs/project/changelog.md b/docs/project/changelog.md index 945e088a1..46aa93be5 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -44,6 +44,14 @@ substitutions: `indexURL` (this was a regression in v0.21.2). {pr}`3077` +- {{ Enhancement }} Pyodide now works with a content security policy that + doesn't include `unsafe-eval`. It is still necessary to include + `wasm-unsafe-eval` (and probably always will be). Since current Safari + versions have no support for `wasm-unsafe-eval`, it is necessary to include + `unsafe-eval` in order to work in Safari. This will likely be fixed in the + next Safari release: https://bugs.webkit.org/show_bug.cgi?id=235408 + {pr}`3075` + - {{ Fix }} Add `url` to list of pollyfilled packages for webpack compatibility. {pr}`3080` diff --git a/src/core/pyproxy.ts b/src/core/pyproxy.ts index 741a94666..b50b4cde7 100644 --- a/src/core/pyproxy.ts +++ b/src/core/pyproxy.ts @@ -131,19 +131,23 @@ type PyProxyCache = { cacheId: number; refcnt: number; leaked?: boolean }; Module.pyproxy_new = function (ptrobj: number, cache?: PyProxyCache) { let flags = Module._pyproxy_getflags(ptrobj); let cls = Module.getPyProxyClass(flags); - // Reflect.construct calls the constructor of Module.PyProxyClass but sets - // the prototype as cls.prototype. This gives us a way to dynamically create - // subclasses of PyProxyClass (as long as we don't need to use the "new - // cls(ptrobj)" syntax). let target; if (flags & IS_CALLABLE) { - // To make a callable proxy, we must call the Function constructor. - // In this case we are effectively subclassing Function. - target = Reflect.construct(Function, [], cls); + // In this case we are effectively subclassing Function in order to ensure + // that the proxy is callable. With a Content Security Protocol that doesn't + // allow unsafe-eval, we can't invoke the Function constructor directly. So + // instead we create a function in the universally allowed way and then use + // `setPrototypeOf`. The documentation for `setPrototypeOf` says to use + // `Object.create` or `Reflect.construct` instead for performance reasons + // but neither of those work here. + target = function () {}; + Object.setPrototypeOf(target, cls.prototype); // Remove undesirable properties added by Function constructor. Note: we // can't remove "arguments" or "caller" because they are not configurable // and not writable + // @ts-ignore delete target.length; + // @ts-ignore delete target.name; // prototype isn't configurable so we can't delete it but it's writable. target.prototype = undefined; diff --git a/src/templates/test_csp.html b/src/templates/test_csp.html new file mode 100644 index 000000000..fd83e4a96 --- /dev/null +++ b/src/templates/test_csp.html @@ -0,0 +1,28 @@ + + + + + pyodide + + + + + + diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py index c512089f3..f00971626 100644 --- a/src/tests/test_pyodide.py +++ b/src/tests/test_pyodide.py @@ -1,4 +1,5 @@ import re +import shutil from collections.abc import Sequence from pathlib import Path from textwrap import dedent @@ -7,8 +8,9 @@ from typing import Any import pytest from pytest_pyodide import run_in_pyodide -from conftest import ROOT_PATH +from conftest import DIST_PATH, ROOT_PATH from pyodide.code import CodeRunner, eval_code, find_imports, should_quiet # noqa: E402 +from pyodide_build.common import get_pyodide_root def _strip_assertions_stderr(messages: Sequence[str]) -> list[str]: @@ -1315,8 +1317,6 @@ def test_relative_index_url(selenium, tmp_path): if version_result.stdout.startswith("v14"): extra_node_args.append("--experimental-wasm-bigint") - import shutil - shutil.copy(ROOT_PATH / "dist/pyodide.js", tmp_dir / "pyodide.js") shutil.copytree(ROOT_PATH / "dist/node_modules", tmp_dir / "node_modules") @@ -1357,3 +1357,18 @@ def test_relative_index_url(selenium, tmp_path): assert result.stdout.strip().split("\n")[-1] == str(ROOT_PATH / "dist") + "/" finally: print_result(result) + + +@pytest.mark.xfail_browsers( + node="Browser only", safari="Safari doesn't support wasm-unsafe-eval" +) +def test_csp(selenium_standalone_noload): + selenium = selenium_standalone_noload + target_path = DIST_PATH / "test_csp.html" + try: + shutil.copy(get_pyodide_root() / "src/templates/test_csp.html", target_path) + selenium.goto(f"{selenium.base_url}/test_csp.html") + selenium.javascript_setup() + selenium.load_pyodide() + finally: + target_path.unlink()