-
-
Save ochafik/64e0595bc6cb778b1c8fcd9457ec877c to your computer and use it in GitHub Desktop.
ES6 Proxy-based Sandbox in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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