Skip to content
This repository was archived by the owner on Sep 6, 2025. It is now read-only.

Commit 4568101

Browse files
authored
Merge pull request #27 from titaniumnetwork-dev/context
WIP window sandboxing
2 parents b777938 + 7b8ae0c commit 4568101

File tree

1 file changed

+210
-16
lines changed

1 file changed

+210
-16
lines changed

src/uv.handler.js

Lines changed: 210 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function __uvHook(window) {
8484
__uv.meta.origin = location.origin;
8585
__uv.location = client.location.emulate(
8686
(href) => {
87-
if (href === 'about:srcdoc') return new URL(href);
87+
if (href.startsWith('about:')) return new URL(href);
8888
if (href.startsWith('blob:')) href = href.slice('blob:'.length);
8989
return new URL(__uv.sourceUrl(href));
9090
},
@@ -106,10 +106,6 @@ function __uvHook(window) {
106106
// websockets
107107
const bareClient = new Ultraviolet.BareClient(__uv$bareURL, __uv$bareData);
108108

109-
if (__uv.location.href === 'about:srcdoc') {
110-
__uv.meta = window.parent.__uv.meta;
111-
}
112-
113109
if (window.EventTarget) {
114110
__uv.addEventListener = window.EventTarget.prototype.addEventListener;
115111
__uv.removeListener = window.EventTarget.prototype.removeListener;
@@ -502,7 +498,7 @@ function __uvHook(window) {
502498
});
503499

504500
client.node.on('baseURI', (event) => {
505-
if (event.data.value.startsWith(window.location.origin))
501+
if (event.data.value.startsWith(__uv.meta.origin))
506502
event.data.value = __uv.sourceUrl(event.data.value);
507503
});
508504

@@ -718,32 +714,230 @@ function __uvHook(window) {
718714
'contentWindow'
719715
).get;
720716

721-
function uvInject(that) {
722-
const win = contentWindowGet.call(that);
723-
717+
/**
718+
*
719+
* @param {typeof globalThis} win
720+
*/
721+
function uvInject(win) {
724722
if (!win.__uv)
725723
try {
726724
__uvHook(win);
727-
} catch (e) {
725+
} catch (err) {
728726
console.error('catastrophic failure');
729-
console.error(e);
727+
console.error(err);
730728
}
731729
}
732730

733731
client.element.hookProperty(HTMLIFrameElement, 'contentWindow', {
734732
get: (target, that) => {
735-
uvInject(that);
736-
return target.call(that);
733+
const win = contentWindowGet.call(that);
734+
uvInject(win);
735+
return sandboxWindow(win);
737736
},
738737
});
739738

740739
client.element.hookProperty(HTMLIFrameElement, 'contentDocument', {
741740
get: (target, that) => {
742-
uvInject(that);
743-
return target.call(that);
741+
const win = contentWindowGet.call(that);
742+
uvInject(win);
743+
try {
744+
return sandboxWindow(win).document;
745+
} catch (err) {
746+
// we are sandboxed, return null
747+
return null;
748+
}
744749
},
745750
});
746751

752+
const sandboxed = new WeakMap();
753+
754+
function illegalSandbox() {
755+
throw new DOMException(
756+
`Blocked a frame with "${__uv.location.origin}" from accessing a cross-origin frame.`
757+
);
758+
}
759+
760+
/**
761+
*
762+
* @template T
763+
* @param {T} object
764+
* @returns {T}
765+
*/
766+
function sandboxObject(object) {
767+
lockProperties(object);
768+
const target = {};
769+
Reflect.setPrototypeOf(target, null);
770+
return new Proxy(target, {
771+
get: (target, prop, receiver) => {
772+
const descriptor = Reflect.getOwnPropertyDescriptor(
773+
object,
774+
prop
775+
);
776+
777+
if (
778+
!(prop in object) ||
779+
(!('value' in descriptor) &&
780+
typeof descriptor.get !== 'function')
781+
)
782+
illegalSandbox();
783+
784+
return Reflect.get(target, prop, receiver);
785+
},
786+
set: (target, prop, value) => {
787+
const descriptor = Reflect.getOwnPropertyDescriptor(
788+
object,
789+
prop
790+
);
791+
792+
if (!(prop in object) || typeof descriptor.set !== 'function')
793+
illegalSandbox();
794+
795+
return Reflect.set(target, prop, value);
796+
},
797+
defineProperty: () => {
798+
illegalSandbox();
799+
},
800+
getOwnPropertyDescriptor: (target, prop, descriptor) => {
801+
if (!(prop in object)) illegalSandbox();
802+
803+
return Reflect.getOwnPropertyDescriptor(
804+
target,
805+
prop,
806+
descriptor
807+
);
808+
},
809+
setPrototypeOf: () => {
810+
illegalSandbox();
811+
},
812+
has: (target, prop) => {
813+
if (!(prop in object)) illegalSandbox();
814+
815+
return true;
816+
},
817+
});
818+
}
819+
820+
const unknownSandboxed = [
821+
'then',
822+
Symbol.toStringTag,
823+
Symbol.hasInstance,
824+
Symbol.isConcatSpreadable,
825+
];
826+
827+
function lockProperties(object) {
828+
Reflect.setPrototypeOf(object, null);
829+
830+
for (const unknown of unknownSandboxed)
831+
Reflect.defineProperty(object, unknown, {
832+
value: undefined,
833+
writable: false,
834+
enumerable: false,
835+
configurable: false,
836+
});
837+
838+
for (const [key, descriptor] of Object.entries(
839+
Object.getOwnPropertyDescriptors(object)
840+
)) {
841+
if (!descriptor.configurable) continue;
842+
843+
descriptor.enumerable = false;
844+
descriptor.configurable = true;
845+
846+
if ('value' in descriptor) {
847+
descriptor.writable = false;
848+
/*if (typeof descriptor.value === 'function')
849+
restrict(descriptor.value);*/
850+
}
851+
852+
/*
853+
if ('get' in descriptor && typeof descriptor.get === 'function')
854+
descriptor.get = restrict(descriptor.get);
855+
856+
if ('set' in descriptor && typeof descriptor.set === 'function')
857+
descriptor.set = restrict(descriptor.set);
858+
*/
859+
860+
Reflect.defineProperty(object, key, descriptor);
861+
}
862+
}
863+
864+
/**
865+
*
866+
* @param {typeof globalThis} win
867+
* @returns {Location}
868+
*/
869+
function sandboxLocation(win) {
870+
return sandboxObject({
871+
set href(value) {
872+
win.__uv.location.href = value;
873+
},
874+
replace(value) {
875+
win.__uv.location.replace(value);
876+
},
877+
});
878+
}
879+
880+
/**
881+
*
882+
* @param {typeof globalThis} win
883+
* @returns {typeof globalThis}
884+
*/
885+
function sandboxWindow(win) {
886+
if (sandboxed.has(win)) return sandboxed.get(win);
887+
if (
888+
new URL(win.__uv.meta.base).origin ===
889+
new URL(window.__uv.meta.base).origin
890+
)
891+
return win;
892+
893+
const obj = {
894+
get window() {
895+
return sandboxedWin;
896+
},
897+
get location() {
898+
return loc;
899+
},
900+
set location(value) {
901+
win.__uv.location.href = value;
902+
},
903+
get closed() {
904+
return win.closed;
905+
},
906+
get frames() {
907+
return sandboxedWin;
908+
},
909+
get length() {
910+
return win.length;
911+
},
912+
get top() {
913+
return win[__uv.methods.top];
914+
},
915+
get opener() {
916+
return sandboxWindow(win.opener);
917+
},
918+
get parent() {
919+
return sandboxWindow(win.parent);
920+
},
921+
blur() {
922+
win.blur();
923+
},
924+
close() {
925+
win.close();
926+
},
927+
focus() {
928+
win.focus();
929+
},
930+
postMessage(...args) {
931+
// todo: remove old workaround for postMessage
932+
win.__uv$setSource(__uv).postMessage(...args);
933+
},
934+
};
935+
const loc = sandboxLocation(win.location);
936+
937+
const sandboxedWin = sandboxObject(obj);
938+
return sandboxedWin;
939+
}
940+
747941
client.element.hookProperty(HTMLIFrameElement, 'srcdoc', {
748942
get: (target, that) => {
749943
return (
@@ -1543,7 +1737,7 @@ function __uvHook(window) {
15431737

15441738
if (this === window) {
15451739
try {
1546-
return '__uv' in val ? val : this;
1740+
return '__uv' in val ? sandboxWindow(val) : this;
15471741
} catch (e) {
15481742
return this;
15491743
}

0 commit comments

Comments
 (0)