Skip to content

Instantly share code, notes, and snippets.

@ochafik
Created September 6, 2016 00:39
Show Gist options
  • Select an option

  • Save ochafik/64e0595bc6cb778b1c8fcd9457ec877c to your computer and use it in GitHub Desktop.

Select an option

Save ochafik/64e0595bc6cb778b1c8fcd9457ec877c to your computer and use it in GitHub Desktop.
ES6 Proxy-based Sandbox in TypeScript
function formatOperation(name: string, ...args: any[]): string {
return name + ' ' + args.map(a => {
try {
return a.toString();
} catch (_) {
return '!';
}
}).join(', ');
}
function unsupported(name: string, ...args: any[]): Error {
return new Error(`Unsupported operation: ${formatOperation(name, ...args)}`);
}
//[...args].map(a => typeof a === 'string' ? a : '?')
const targetSymbol = Symbol();
declare var global: any;
var rootObject = typeof window === 'undefined' ? global : window;
function wrap<T>(object: T, allowedApis: any): T {
if (object == null ||
typeof object === 'string' ||
typeof object === 'number' ||
typeof object === 'boolean' ||
typeof object === 'symbol') {
return object;
}
// if (object !== rootObject) {
// console.log('Wrapping: ', object);
// }
if (object instanceof Function) {
let wrappedFunction = function(...args) {
return wrap(object.apply(rootObject, args));
};
wrappedFunction[targetSymbol] = object;
return wrappedFunction as T;
}
return new Proxy<Proxied<T>>(new Proxied<T>(object), new SandboxProxyHandler<T>(allowedApis)) as T;
}
const getter = Symbol();
const setter = Symbol();
const writable = Symbol();
type Access<T, P> = {
[writable]: boolean,
[getter]: (undefined | ((target: T) => P)),
[setter]: (undefined | ((target: T, value: P) => void)),
};
class Proxied<T> {
constructor(public value: T, public allowedApis: any) {}
}
class SandboxProxyHandler<T> implements ProxyHandler<Proxied<T>> {
constructor(private allowedApis: any) {}
private getAccess(p: PropertyKey): (Access<T, any> | undefined) {
return this.allowedApis[p] as Access<T, any>;
}
log<R>(args: any[], action: () => R): R {
let invocation = formatOperation.apply(null, args);
let formattedResult: string;
try {
let result = action();
if ((typeof result === 'object' || typeof result === 'function') && targetSymbol in result) {
formattedResult = `Proxy(${result[targetSymbol]})`;
} else {
formattedResult = `${result}`;
}
return result;
} catch (e) {
formattedResult = `threw ${e}`;
throw e;
} finally {
console.log(`${invocation} -> ${formattedResult}`);
}
}
getPrototypeOf(target: Proxied<T>): any {
return this.log(['getPrototypeOf'], () => {
// throw unsupported('getPrototypeOf');
// return wrap(Object.getPrototypeOf(target), this.allowedApis);
return Object.getPrototypeOf(target);
});
}
setPrototypeOf(target: Proxied<T>, v: any): boolean {
return this.log(['setPrototypeOf', v], () => {
throw unsupported('setPrototypeOf', v);
});
}
isExtensible(target: Proxied<T>): boolean {
return this.log(['isExtensible'], () => {
throw unsupported('isExtensible');
});
}
preventExtensions(target: Proxied<T>): boolean {
return this.log(['preventExtensions'], () => {
throw unsupported('preventExtensions');
});
}
getOwnPropertyDescriptor(target: Proxied<T>, p: PropertyKey): PropertyDescriptor {
return this.log(['getOwnPropertyDescriptor', p], () => {
// throw unsupported('getOwnPropertyDescriptor', p);
let access = this.getAccess(p);
if (!access) {
return undefined as any as PropertyDescriptor;
}
return Object.getOwnPropertyDescriptor(target.value, p);
});
}
has(target: Proxied<T>, p: PropertyKey): boolean {
if (p === targetSymbol) {
return true;
}
return this.log(['has', p], () => {
// throw unsupported('has', p);
return p in target.value;
// THIS IS A BUG: if we return has = false, then get is never called!.
// let access = this.getAccess(p);
// if (!access) {
// return false;
// }
// return p in target.value;
});
}
get(target: Proxied<T>, p: PropertyKey, receiver: any): any {
if (p === targetSymbol) {
return target.value;
}
// if (p === Symbol.unscopables) {
// return {
// 'console': false
// };
// }
return this.log(['get', p], () => {
// throw unsupported('get', p);
let access = this.getAccess(p);
if (!access) {
return undefined;
}
let targetValue = target.value;
let member: any;
if (access[getter]) {
member = access[getter](targetValue);
} else {
member = targetValue[p];
}
if (member instanceof Function) {
member = member.bind(targetValue);
}
return wrap(member, access);
});
}
set(target: Proxied<T>, p: PropertyKey, value: any, receiver: any): boolean {
return this.log(['set', value], () => {
// throw unsupported('set', value);
let access = this.getAccess(p);
if (!access || !access.writable) {
return false;
}
let targetValue = target.value;
if (access[setter]) {
access[setter](targetValue, value);
} else {
targetValue[p] = value;
}
return true;
});
}
deleteProperty(target: Proxied<T>, p: PropertyKey): boolean {
return this.log(['deleteProperty', p], () => {
// throw unsupported('deleteProperty', p);
let access = this.getAccess(p);
if (!access || !access[writable]) {
// TODO(ochafik): Add Access.configurable?
return false;
}
return delete target.value[p];
});
}
defineProperty(target: Proxied<T>, p: PropertyKey, attributes: PropertyDescriptor): boolean {
return this.log(['defineProperty', p], () => {
throw unsupported('defineProperty', p);
});
}
enumerate(target: Proxied<T>): PropertyKey[] {
return this.log(['enumerate'], () => {
throw unsupported('enumerate');
});
}
ownKeys(target: Proxied<T>): PropertyKey[] {
return this.log(['ownKeys'], () => {
throw unsupported('ownKeys');
});
}
apply(target: Proxied<T>, thisArg: any, argArray?: any): any {
return this.log(['apply', ...(argArray || [])], () => {
throw unsupported('apply', ...(argArray || []));
// return target.value.apply(thisArg, argArray);
});
}
construct(target: Proxied<T>, thisArg: any, argArray?: any): any {
return this.log(['construct', ...(argArray || [])], () => {
return wrap(new ((target.value as any as Function).bind(thisArg))(...argArray), this.allowedApis);
});
}
}
const root = wrap(rootObject, {
XmlHttpRequest: {
open: {},
onstatuschange: {},
},
Date: {
now: {},
},
window: {
[getter]() {
return root;
}
},
document: {
write: {},
},
console: {
log: {
[getter]() {
return function(msg: string) {
console.log(msg);
}
}
}
},
setTimeout: {},
// getter: function(window) {
// return function(f, milliseconds, ...args) {
// window.setTimeout(function(...args) {
// with
// }, milliseconds, ...args);
// }
//
// }
// }
});
Function(`with(arguments[0]) {
document.write('hey!');
console.log('NONONO');
class Baz {
bam() {
console.log("BAAAAM");
}
}
console.log('AUDIO: ' + window.Audio);
new Baz().bam();
setTimeout(() => console.log('Foo!!!'), 1000);
Function('alert()');
}`)(root);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment