Skip to content

Commit 1b6692a

Browse files
committed
feat: implement facade layers for window and document objects to virtualize frame property access and messaging sources
1 parent 6b2ae7e commit 1b6692a

4 files changed

Lines changed: 278 additions & 29 deletions

File tree

test/e2e/expected-deltas.json

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,6 @@
77
"policyHeaders.reportOnly": {
88
"proxy": "",
99
"native": "default-src 'none'; connect-src 'none'"
10-
},
11-
"surface.frameDocument.contentWindowHref": {
12-
"proxy": "<zeroproxy-frame-route>",
13-
"native": "<target-frame-url>"
14-
},
15-
"surface.frameSrcdoc.contentDocumentURL": {
16-
"proxy": "<target-document-url>",
17-
"native": "<srcdoc-url>"
18-
},
19-
"surface.frameSrcdoc.sourceIsFrame": {
20-
"proxy": false,
21-
"native": true
2210
}
2311
}
2412
}

web/runtime-prelude.mjs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
6666
const frameSandboxMeta = new WeakMap();
6767
const directExternalFrameWindowOrigins = new WeakMap();
6868
const crossWindowProxyCache = new WeakMap();
69+
const frameWindowFacades = new WeakMap();
70+
const frameElementWindowFacades = new WeakMap();
71+
const frameSrcdocMessageSources = new WeakMap();
72+
const frameDocumentFacades = new WeakMap();
6973
const postMessageWrappers = new WeakMap();
7074
const frameTargetOriginMarker = Symbol.for('zeroproxy.frame.targetOrigin');
7175
const networkContainmentMarker = Symbol.for('zeroproxy.network.contained');
@@ -76,6 +80,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
7680
const rewrittenInlineScripts = new WeakSet();
7781
const rewrittenStyleNodes = new WeakSet();
7882
const documentWriteHookedWindows = new WeakSet();
83+
const messageEventSourceHookedPrototypes = new WeakSet();
7984
const windowMethodBindings = new Map();
8085
const integrityBackupAttr = 'data-zp-integrity';
8186
const nonceBackupAttr = 'data-zp-target-nonce';
@@ -608,6 +613,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
608613
frameTargetOriginMarker,
609614
maskNativeFunction,
610615
isDirectExternalFrameElement,
616+
messageSourceFacadeFor,
611617
});
612618
const { installChildRewriteHelpers } = createChildRewriteHelpers({
613619
root,
@@ -629,6 +635,202 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
629635
frameSandboxMeta,
630636
isDirectExternalFrameElement,
631637
});
638+
639+
function frameDocumentURL(frame, childDoc, childWin) {
640+
try {
641+
if (frame && Native.getAttribute.call(frame, 'srcdoc') != null) return 'about:srcdoc';
642+
} catch {}
643+
try {
644+
const target = urlMeta.get(frame) || Native.getAttribute.call(frame, 'data-zp-target-url') || '';
645+
if (target) return target;
646+
} catch {}
647+
try {
648+
const href = childWin && childWin.location && childWin.location.href;
649+
if (href) return String(href);
650+
} catch {}
651+
try {
652+
const href = childDoc && childDoc.URL;
653+
if (href) return String(href);
654+
} catch {}
655+
return 'about:blank';
656+
}
657+
658+
function isSrcdocFrame(frame) {
659+
try { return !!frame && Native.getAttribute.call(frame, 'srcdoc') != null; }
660+
catch { return false; }
661+
}
662+
663+
function frameLocationFacadeFor(frame, childDoc, childWin) {
664+
const current = () => {
665+
try { return new URL(frameDocumentURL(frame, childDoc, childWin)); }
666+
catch { return new URL('about:blank'); }
667+
};
668+
const locationFacade = {
669+
get href() { return current().href; },
670+
set href(_v) {},
671+
get protocol() { return current().protocol; },
672+
get host() { return current().host; },
673+
get hostname() { return current().hostname; },
674+
get port() { return current().port; },
675+
get pathname() { return current().pathname; },
676+
get search() { return current().search; },
677+
get hash() { return current().hash; },
678+
set hash(_v) {},
679+
get origin() { return current().origin; },
680+
assign(_v) {},
681+
replace(_v) {},
682+
reload() {},
683+
toString() { return current().href; },
684+
valueOf() { return current().href; },
685+
[Symbol.toPrimitive]() { return current().href; }
686+
};
687+
try { Object.defineProperty(locationFacade, Symbol.toStringTag, { value: 'Location', enumerable: false, configurable: true }); } catch {}
688+
maskMethods(locationFacade, ['assign','replace','reload','toString','valueOf']);
689+
maskNativeFunction(locationFacade[Symbol.toPrimitive], Symbol.toPrimitive);
690+
try { Object.freeze(locationFacade); } catch {}
691+
return locationFacade;
692+
}
693+
694+
function frameWindowValue(frame, childWin, proxy, locationFacade, prop) {
695+
if (prop === Symbol.toStringTag) return 'Window';
696+
if (prop === 'window' || prop === 'self' || prop === 'globalThis' || prop === 'frames') return proxy;
697+
if (prop === 'location') return locationFacade;
698+
if (prop === 'origin') return locationFacade.origin;
699+
if (prop === 'postMessage') return postMessageWrapperFor(childWin);
700+
if (prop === 'document') {
701+
try { return frameDocumentFacadeFor(frame, childWin.document, childWin); } catch { return undefined; }
702+
}
703+
if (prop === 'parent' || prop === 'top') {
704+
if (isSrcdocFrame(frame)) return root;
705+
try {
706+
const value = childWin[prop];
707+
if (!value || value === childWin) return proxy;
708+
if (value === root) return root;
709+
return frameWindowFacades.get(value) || value;
710+
} catch {
711+
return root;
712+
}
713+
}
714+
if (prop === 'opener') {
715+
try {
716+
const value = childWin.opener;
717+
if (!value) return null;
718+
if (value === root) return root;
719+
return frameWindowFacades.get(value) || value;
720+
} catch {
721+
return null;
722+
}
723+
}
724+
const value = childWin[prop];
725+
return typeof value === 'function' && WINDOW_BOUND_METHODS.has(prop) ? value.bind(childWin) : value;
726+
}
727+
728+
function frameWindowFacadeFor(frame, childWin, forceFacade = false) {
729+
if (!childWin) return childWin;
730+
if (!forceFacade && isSrcdocFrame(frame)) {
731+
try { return frameSrcdocMessageSources.get(frame) || childWin; }
732+
catch { return childWin; }
733+
}
734+
try {
735+
const existing = frame && frameElementWindowFacades.get(frame);
736+
if (existing) return existing;
737+
} catch {}
738+
if (frameWindowFacades.has(childWin)) return frameWindowFacades.get(childWin);
739+
let proxy;
740+
const locationFacade = frameLocationFacadeFor(frame, null, childWin);
741+
proxy = new Proxy({}, {
742+
get(_target, prop) { return frameWindowValue(frame, childWin, proxy, locationFacade, prop); },
743+
set(_target, prop, value) {
744+
if (prop === 'location') return true;
745+
try { childWin[prop] = value; return true; } catch { return false; }
746+
},
747+
has(_target, prop) {
748+
return prop === 'location' || prop === 'document' || prop === 'parent' || prop === 'top' || prop in childWin;
749+
},
750+
getOwnPropertyDescriptor(_target, prop) {
751+
if (prop === 'location' || prop === 'document' || prop === 'parent' || prop === 'top') {
752+
return { configurable: true, enumerable: true, get() { return frameWindowValue(frame, childWin, proxy, locationFacade, prop); } };
753+
}
754+
try { return Reflect.getOwnPropertyDescriptor(childWin, prop); } catch { return undefined; }
755+
},
756+
ownKeys() {
757+
try { return Reflect.ownKeys(childWin); } catch { return []; }
758+
}
759+
});
760+
membraneRawTargets.set(proxy, childWin);
761+
frameWindowFacades.set(childWin, proxy);
762+
try { if (frame) frameElementWindowFacades.set(frame, proxy); } catch {}
763+
return proxy;
764+
}
765+
766+
function frameDocumentValue(frame, childDoc, childWin, windowFacade, locationFacade, prop) {
767+
if (prop === Symbol.toStringTag) return 'HTMLDocument';
768+
if (prop === 'defaultView') return windowFacade;
769+
if (prop === 'location') return locationFacade;
770+
if (prop === 'URL' || prop === 'documentURI') return locationFacade.href;
771+
const value = childDoc[prop];
772+
return typeof value === 'function' ? value.bind(childDoc) : value;
773+
}
774+
775+
function frameDocumentFacadeFor(frame, childDoc, childWin) {
776+
if (!childDoc) return childDoc;
777+
if (frameDocumentFacades.has(childDoc)) return frameDocumentFacades.get(childDoc);
778+
const rawWindow = childWin || childDoc.defaultView;
779+
const windowFacade = frameWindowFacadeFor(frame, rawWindow);
780+
const locationFacade = frameLocationFacadeFor(frame, childDoc, rawWindow);
781+
const proxy = new Proxy({}, {
782+
get(_target, prop) { return frameDocumentValue(frame, childDoc, rawWindow, windowFacade, locationFacade, prop); },
783+
set(_target, prop, value) {
784+
try { childDoc[prop] = value; return true; } catch { return false; }
785+
},
786+
has(_target, prop) {
787+
return prop === 'defaultView' || prop === 'URL' || prop === 'documentURI' || prop in childDoc;
788+
},
789+
getOwnPropertyDescriptor(_target, prop) {
790+
if (prop === 'defaultView' || prop === 'URL' || prop === 'documentURI') {
791+
return { configurable: true, enumerable: true, get() { return frameDocumentValue(frame, childDoc, rawWindow, windowFacade, locationFacade, prop); } };
792+
}
793+
try { return Reflect.getOwnPropertyDescriptor(childDoc, prop); } catch { return undefined; }
794+
},
795+
ownKeys() {
796+
try { return Reflect.ownKeys(childDoc); } catch { return []; }
797+
}
798+
});
799+
membraneRawTargets.set(proxy, childDoc);
800+
frameDocumentFacades.set(childDoc, proxy);
801+
return proxy;
802+
}
803+
804+
function messageSourceFacadeFor(source, ev) {
805+
if (!source) return srcdocMessageSourceFacade(ev);
806+
if (frameWindowFacades.has(source)) return frameWindowFacades.get(source);
807+
try {
808+
const frames = document.querySelectorAll && document.querySelectorAll('iframe,frame');
809+
if (!frames) return source;
810+
for (let i = 0; i < frames.length; i++) {
811+
const facade = frames[i].contentWindow;
812+
const raw = membraneRawTargets.get(facade) || facade;
813+
if (raw === source) return facade;
814+
}
815+
} catch {}
816+
return srcdocMessageSourceFacade(ev, source) || source;
817+
}
818+
819+
function srcdocMessageSourceFacade(ev, source) {
820+
try {
821+
if (!ev) return null;
822+
const frames = document.querySelectorAll && document.querySelectorAll('iframe[srcdoc],frame[srcdoc]');
823+
if (!frames || frames.length !== 1) return null;
824+
if (source) {
825+
const facade = frameWindowFacadeFor(frames[0], source, true);
826+
frameSrcdocMessageSources.set(frames[0], facade);
827+
return facade;
828+
}
829+
return frames[0].contentWindow || null;
830+
} catch {
831+
return null;
832+
}
833+
}
632834
try { Object.defineProperty(root, frameTargetOriginMarker, { get() { return virtualURL.origin; }, enumerable: false, configurable: false }); } catch {}
633835
installToStringMasking(root);
634836
define(root, '__ZP_SET_BASE', updateVirtualBase);
@@ -800,6 +1002,14 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
8001002
if (base === document && (prop === 'URL' || prop === 'documentURI')) return virtualURL.href;
8011003
if (base === document && prop === 'baseURI') return baseURL;
8021004
if (base === document && prop === 'referrer') return boot.documentReferrer || '';
1005+
if (prop === 'source' && base && typeof base === 'object') {
1006+
try {
1007+
const rawSource = Reflect.get(Object(base), prop);
1008+
const framedSource = messageSourceFacadeFor(rawSource, base);
1009+
if (framedSource) return framedSource;
1010+
return rawSource;
1011+
} catch {}
1012+
}
8031013
if (isWindowLike(base)) {
8041014
if (prop === 'window' || prop === 'self' || prop === 'globalThis' || prop === 'frames') return base === scope || base === root ? scope : base;
8051015
if (prop === 'top' || prop === 'parent' || prop === 'opener') {
@@ -1403,6 +1613,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
14031613
const addEventListener = w && w.addEventListener && w.addEventListener.bind(w);
14041614
const removeEventListener = w && w.removeEventListener && w.removeEventListener.bind(w);
14051615
if (!addEventListener || !removeEventListener) return;
1616+
installMessageEventSourceAccessor(w);
14061617
function wrap(listener) {
14071618
if (!listener || (typeof listener !== 'function' && typeof listener.handleEvent !== 'function')) return listener;
14081619
if (messageListenerWrappers.has(listener)) return messageListenerWrappers.get(listener);
@@ -1427,6 +1638,19 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
14271638
});
14281639
}
14291640

1641+
function installMessageEventSourceAccessor(w) {
1642+
const proto = w && w.MessageEvent && w.MessageEvent.prototype;
1643+
if (!proto) return;
1644+
if (messageEventSourceHookedPrototypes.has(proto)) return;
1645+
const d = Object.getOwnPropertyDescriptor(proto, 'source');
1646+
if (!d || typeof d.get !== 'function') return;
1647+
messageEventSourceHookedPrototypes.add(proto);
1648+
defineAccessor(proto, 'source', function() {
1649+
const raw = d.get.call(this);
1650+
return messageSourceFacadeFor(raw, this) || raw;
1651+
});
1652+
}
1653+
14301654
function visibleResourceURL(el, attrName) {
14311655
return urlMeta.get(el) || Native.getAttribute.call(el, 'data-zp-target-url') || Native.getAttribute.call(el, attrName) || '';
14321656
}
@@ -2752,6 +2976,8 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
27522976
networkContainmentMarker,
27532977
isDirectExternalFrameElement,
27542978
installNetworkContainment,
2979+
frameWindowFacadeFor,
2980+
frameDocumentFacadeFor,
27552981
});
27562982
const nativeCreateElement = w === root ? Native.createElement : w.document.createElement.bind(w.document);
27572983
const nativeCreateElementNS = w === root ? Native.createElementNS : w.document.createElementNS && w.document.createElementNS.bind(w.document);

web/runtime/frames/accessors.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export function createFrameAccessors({
22
networkContainmentMarker,
33
isDirectExternalFrameElement,
44
installNetworkContainment,
5+
frameWindowFacadeFor,
6+
frameDocumentFacadeFor,
57
}) {
68
const instrumentedWindows = new WeakSet();
79

@@ -44,16 +46,18 @@ export function createFrameAccessors({
4446
function contentWindowGetter(nativeGet) {
4547
return function contentWindow() {
4648
const childWin = nativeGet.call(this);
47-
return isDirectExternalFrameElement(this) ? childWin : containFrameWindow(childWin, this);
49+
if (isDirectExternalFrameElement(this)) return childWin;
50+
const contained = containFrameWindow(childWin, this);
51+
return frameWindowFacadeFor ? frameWindowFacadeFor(this, contained) : contained;
4852
};
4953
}
5054

5155
function contentDocumentGetter(nativeGet) {
5256
return function contentDocument() {
5357
const childDoc = nativeGet.call(this);
54-
if (childDoc && childDoc.defaultView && !isDirectExternalFrameElement(this)) {
55-
containFrameWindow(childDoc.defaultView, this);
56-
}
58+
if (!childDoc || isDirectExternalFrameElement(this)) return childDoc;
59+
const childWin = childDoc.defaultView ? containFrameWindow(childDoc.defaultView, this) : null;
60+
if (frameDocumentFacadeFor) return frameDocumentFacadeFor(this, childDoc, childWin);
5761
return childDoc;
5862
};
5963
}

0 commit comments

Comments
 (0)