Fix creation of PyProxy when CSP without unsafe-eval is used (#3075)

See discussion following #2432 (comment).
This commit is contained in:
Hood Chatham 2022-09-08 17:03:11 -05:00 committed by GitHub
parent 60714a9cf0
commit fbed5b0cf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 65 additions and 10 deletions

View File

@ -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`

View File

@ -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;

View File

@ -0,0 +1,28 @@
<!-- Bootstrap HTML for running the unit tests. -->
<!DOCTYPE html>
<html>
<head>
<title>pyodide</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src http: 'wasm-unsafe-eval'"
/>
<script type="text/javascript">
window.logs = [];
console.log = function (message) {
window.logs.push(message);
};
console.warn = function (message) {
window.logs.push(message);
};
console.info = function (message) {
window.logs.push(message);
};
console.error = function (message) {
window.logs.push(message);
};
</script>
<script src="./pyodide.js"></script>
</head>
<body></body>
</html>

View File

@ -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()