Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 18 additions & 32 deletions proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,32 @@
* the proxy or its target before changing the target's prototype.
*/
export function createProxy(NativeProxy) {
const reflectApply = Reflect.apply;
const reflectGetPrototypeOf = Reflect.getPrototypeOf;
const reflectSetPrototypeOf = Reflect.setPrototypeOf;
const { getPrototypeOf, setPrototypeOf } = Reflect;
const objectCreate = Object.create;
const TypeErrorCtor = TypeError;
const proxies = new WeakSet();
const { add, has } = WeakSet.prototype;

// Stop at other proxies created by this wrapper. Unrecognized proxies are
// traversed, which can invoke page-owned getPrototypeOf traps unlike native checks.
const isCyclic = (proxy, target, proto) => {
const stops = (p) => p === null || p === proxy || p === target || reflectApply(has, proxies, [p]);
// Proxies are walked like any other object, so a cycle spanning several scriptlet
// proxies is found. The tortoise ends walks over chains that are already cyclic.
const reaches = (proto, proxy, target) => {
let slow = proto;
let fast = proto;
// Classic "tortoise and hare" cycle check
while (!stops(fast)) {
fast = reflectGetPrototypeOf(fast);
if (stops(fast)) break;
fast = reflectGetPrototypeOf(fast);
slow = reflectGetPrototypeOf(slow);
if (slow === fast) return false;
for (let i = 1; fast !== null; i++) {
if (fast === proxy || fast === target) return true;
fast = getPrototypeOf(fast);
if (i % 2 === 0) {
slow = getPrototypeOf(slow);
if (slow === fast) return false;
}
}
return fast === proxy || fast === target;
return false;
};

return function Proxy(target, handler) {
if (new.target === undefined) {
throw new TypeErrorCtor("Constructor Proxy requires 'new'");
}
let proxy;
// Inherit from the handler rather than copy it, so traps defined on a prototype still apply
if (handler instanceof Object && handler.setPrototypeOf === undefined) {
// Returning false makes Object.setPrototypeOf throw while Reflect.setPrototypeOf
// returns false. The error message differs from native cycle rejection.
const setPrototypeOf = (target, proto) =>
!(proto !== null && isCyclic(proxy, target, proto)) && reflectSetPrototypeOf(target, proto);
handler = objectCreate(handler, { setPrototypeOf: { value: setPrototypeOf } });
}
proxy = new NativeProxy(target, handler);
reflectApply(add, proxies, [proxy]);
// Inherit from the handler rather than copy it, so traps added to it later still apply.
const proxy = new NativeProxy(target, objectCreate(handler, {
// Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf
// return false, as both do for a native function.
setPrototypeOf: { value: (target, proto) => !reaches(proto, proxy, target) && setPrototypeOf(target, proto) },
}));
return proxy;
};
}
16 changes: 16 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ suite("proxied functions", () => {
});
}

test("rejects a cycle spanning two proxied functions like native functions", () => {
const probeBoth = (a, b) => `
(() => {
const a = ${a};
const b = ${b};
Object.setPrototypeOf(a, Object.create(b));
let error;
try { Object.setPrototypeOf(b, Object.create(a)); } catch (e) { error = e; }
return { error: error?.name, reflect: Reflect.setPrototypeOf(b, Object.create(a)), call: typeof b.call };
})()
`;
const expected = vm.runInContext(probeBoth("function a() {}", "function b() {}"), vm.createContext({}));
const page = createPage("prevent-setTimeout.js", "x");
assert.deepStrictEqual({ ...vm.runInContext(probeBoth("setTimeout", "Function.prototype.toString"), page) }, { ...expected });
});

test("still sets a non-cyclic prototype", () => {
const page = createPage("prevent-setTimeout.js", "x");
const set = "const proto = Object.create(Function.prototype); Object.setPrototypeOf(setTimeout, proto) === setTimeout && Object.getPrototypeOf(setTimeout) === proto";
Expand Down
54 changes: 20 additions & 34 deletions ubo.js
Original file line number Diff line number Diff line change
Expand Up @@ -810,46 +810,32 @@ fn: 'trustedOverrideElementMethod',
// Proxy shadows the global for every scriptlet in run(), see proxy.js.
export function run(scriptletGlobals, calls) {
const Proxy = (function createProxy(NativeProxy) {
const reflectApply = Reflect.apply;
const reflectGetPrototypeOf = Reflect.getPrototypeOf;
const reflectSetPrototypeOf = Reflect.setPrototypeOf;
const { getPrototypeOf, setPrototypeOf } = Reflect;
const objectCreate = Object.create;
const TypeErrorCtor = TypeError;
const proxies = new WeakSet();
const { add, has } = WeakSet.prototype;

// Stop at other proxies created by this wrapper. Unrecognized proxies are
// traversed, which can invoke page-owned getPrototypeOf traps unlike native checks.
const isCyclic = (proxy, target, proto) => {
const stops = (p) => p === null || p === proxy || p === target || reflectApply(has, proxies, [p]);

// Proxies are walked like any other object, so a cycle spanning several scriptlet
// proxies is found. The tortoise ends walks over chains that are already cyclic.
const reaches = (proto, proxy, target) => {
let slow = proto;
let fast = proto;
// Classic "tortoise and hare" cycle check
while (!stops(fast)) {
fast = reflectGetPrototypeOf(fast);
if (stops(fast)) break;
fast = reflectGetPrototypeOf(fast);
slow = reflectGetPrototypeOf(slow);
if (slow === fast) return false;
}
return fast === proxy || fast === target;
for (let i = 1; fast !== null; i++) {
if (fast === proxy || fast === target) return true;
fast = getPrototypeOf(fast);
if (i % 2 === 0) {
slow = getPrototypeOf(slow);
if (slow === fast) return false;
}
}
return false;
};

return function Proxy(target, handler) {
if (new.target === undefined) {
throw new TypeErrorCtor("Constructor Proxy requires 'new'");
}
let proxy;
// Inherit from the handler rather than copy it, so traps defined on a prototype still apply
if (handler instanceof Object && handler.setPrototypeOf === undefined) {
// Returning false makes Object.setPrototypeOf throw while Reflect.setPrototypeOf
// returns false. The error message differs from native cycle rejection.
const setPrototypeOf = (target, proto) =>
!(proto !== null && isCyclic(proxy, target, proto)) && reflectSetPrototypeOf(target, proto);
handler = objectCreate(handler, { setPrototypeOf: { value: setPrototypeOf } });
}
proxy = new NativeProxy(target, handler);
reflectApply(add, proxies, [proxy]);
// Inherit from the handler rather than copy it, so traps added to it later still apply.
const proxy = new NativeProxy(target, objectCreate(handler, {
// Returning false makes Object.setPrototypeOf throw and Reflect.setPrototypeOf
// return false, as both do for a native function.
setPrototypeOf: { value: (target, proto) => !reaches(proto, proxy, target) && setPrototypeOf(target, proto) },
}));
return proxy;
};
})(globalThis.Proxy);
Expand Down