Sandbox In-Depth

Scott Waldron Updated by Scott Waldron

Why have a sandbox at all? Consider this business rule engine written to run in IronPython. This engine allows adding “commands” which the user can execute in a context. Here is the relevant Python code:

# python code
def cmdReflect():
bflags = BindingFlags.Instance | BindingFlags.NonPublic
db = DbFactory.Current.Create()
field = db.GetType().BaseType.BaseType.GetField(
'm_ConnectionStringFunc', bflags)
return field.GetValue(db)()

When the user clicks the “cmdReflect” command that little snippet of code shows this gem on the user’s browser:

Bad code? Sure – it’s retrieving a string that contains a password. But these types of issues abound in many frameworks. Credit card numbers, social security numbers, unencrypted passwords… they are stored in-memory in many systems.

Even if your code is airtight and doesn’t engage in this bad behavior, do you really want a clever customer to be able to write a script that shows the names of all the private methods, and then figure out how to call them with Invoke()?

No Reflection

Every instance of every object in the native language has a GetType() method that returns reflected type information – and not just a little bit: my informal count of the instance members of System.Type (the return value from calling that innocuous-looking GetType) came up with well over one hundred methods and properties. If you know your way around, pretty much any kind of introspection is possible. And there is not much you can do about it – if you look online you will find all kinds of suggestions about obfuscation, etc. But obfuscation is just a band-aid – and a painful one at that.

So, what do we mean by “no reflection”? Can Dino call GetType()? Sure. But try to evaluate something based on the return value of that GetType() call, and it will fail. The practical implications of this are big: It means you can continue to work with System.Type instances (for example, passing them to a native method that takes a Type parameter) but never get access to the reflected information about the type. Consider these two native methods, which Dino has no problem calling:

// this is the one with generic args, created directly
public IRepository<T> GetRepository<T>()
{
return RepositoryProvider.GetRepo<T>();
}
// this method takes a type instance, maybe it's created with reflection
public IRepository GetRepository( Type t )
{
return RepositoryProvider.GetRepo( t );
}

Dino code might call these native methods as follows:

from Db import RepositoryProvider as @db;
from DbTypes import Order; // script writer sandbox has access to Order type

// Dino can call both methods without typeof!
var repo = @db.GetRepository[Order](); // type symbol as generic arg
var repo2 = @db.GetRepository(Order); // type symbol as regular arg
var newOrd = Order('Widget', 25);
var repo3 = @db.GetRepository(newOrd.GetType()); // yep, still ok
var repo4 = @db.GetRepository[newOrd.GetType()](); // weird, but still works!

// now let's try to hack some information…
var fields = newOrd.GetType().GetFields(); // error, missing member 'GetFields'

A clever bit of wrangling that Dino does on your behalf is to automatically accept type symbols in place of types for native methods that take a System.Type as an argument or generic type argument. This obviates the need for any typeof operator in Dino. This is completely transparent: the expression evaluator translates all arguments going out to native code from a Dino ITypeSymbol to Type, and translates all return values from native code into Dino from Type to ITypeSymbol. Dino is in fact so insistent that a Type instance never shows up, that if it finds one as the value for a symbol, or even on the evaluation stack, it will throw an exception. Setting a variable from “outside” to a Type instance will automatically convert the value to an ITypeSymbol.

The Dino TypeSymbol class defines an “implicit conversion” to System.Type, so passing the result of a Dino evaluation to a native method that expects a Type will succeed.

public static implicit operator Type( TypeSymbol ts )
{
if( null == ts )
return null;

return ts.Type;
}

Dino can even work with the System.Type type, assuming the sandbox allows import:

[TestMethod]
public void CanWorkWithTypeType()
{
var result = DinoCompiler.Evaluate(
"from System import Type; Type.GetType('System.String');",
new AllowAllSandbox() );
Assert.AreEqual( ( (ITypeSymbol)result ).Type, typeof( string ));
}

The best result of this is that you just don’t need to think about it: you (and your script-writing customers) can continue to call GetType() and pass type instances around – internally, to external methods and properties, as generic parameters (try it – it works: see the fun unit test below). But you (and more importantly a hacker) can’t go into the netherworld of System.Type itself.

[TestMethod]
public void GetTypeCanBeUsedAsGenericTypeParam()
{
var result = DinoCompiler.Evaluate(
@"

from System import int;
from System.Collections.Generic import IEnumerable;

// yep, you can even use GetType() for a generic type param
var @list = list[0.GetType()](0, 1, 2);

@list is IEnumerable[int];

", null, new AllowAllSandbox() );

Assert.IsTrue( result );
}

The ISandbox Interface

So now we know that banning reflection is an intrinsic part of the Dino sandbox: it simply isn’t possible without using special native types that are designed to allow reflection. It doesn’t matter which sandbox you use, or none: reflection is not allowed using the methods and properties of System.Type.

With that out of the way, let’s look at what else the compile-time sandbox can restrict. We’ll start by looking at the interface, then go on to some specific examples of sandboxes and the theory behind each one.

/// <summary>
/// Adds the ability to restrict types from
/// a script definition.
/// </summary>
public interface ISandbox
{
/// <summary>
/// Gets a value indicating if script imports are supported.
/// </summary>
bool AllowScriptImport
{
get;
}

/// <summary>
/// Indicates that the script can access this type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
bool AllowType( Type type );
}

Who Can Write, Not Who Can Execute

The Dino sandbox is primarily concerned with what kind of code a user can write, not what code a user (or system) can execute. Presumably, you are giving your users the ability to write custom scripts. It is up to them to decide who can execute what, simply by creating their own security layer around an executable function.

Sandboxes that are checked at run time can certainly enforce security, but they lead to so many errors that users eventually give up. For example, should an event handler method really check the sandbox at run time? Who knows what will trigger that code, now or in the future? The code was written for a reason, and it should execute regardless of the user. However, should that same user be able to edit the script and add references to System.IO.File? Probably not.

Deny All (White List)

We'll be expanding the documentation in this section soon. Please contact us if you have questions.

Allow All (Black List)

We'll be expanding the documentation in this section soon. Please contact us if you have questions.

The SandboxProvider Class

We'll be expanding the documentation in this section soon. Please contact us if you have questions.

SandboxRecommendations

Avoid Allowing Reflection

It is strongly suggested that you always use a sandbox that prohibits introspection (reflection). Although Dino intrinsically prohibits reflection, it is of course possible to import a native type that takes the place of all the System.Type reflection members. Think twice about doing this though: it should not be the purview of a scripting language to need to check a lot of type information – Dino is an untyped language. Since native type information is irrelevant much of the time in a dynamically-typed language, reflection adds an unnecessary security hole.

If the user needs it, Dino natively allows decision-making based on type information using the is operator, e.g. “a is String”.

Sequester Types with Namespaces

Restricting access to certain native types is one of the most critical aspects of sandboxing. For database types, it is critical to sequester “admin-only” types of data structures and DTOs in their own namespace.

Dynamic Sandboxing

Dino defines a special function called __sandbox__ that can be used to incrementally expand the functionality of downstream scripts that may need to relax restrictions.

For example, you may start out only allowing users to do the basics. But there will come a time when they want to perhaps access the database with a SQL query, etc. By using dynamic sandboxing, you can ensure that the functionality which they can access is fine-grained and remains in your control.

Dynamic sandboxing works by defining a “base” script that has the special __sandbox__ function defined. This function takes no parameters and should return an ISandbox instance.

// sandbox.dino
from MyCode import MySandbox;

def __sandbox__() {
return MySandbox.Default;
}

// userscript.dino
import script 'sandbox.dino';

How did we do?

Dino Cookbook

Functions as Delegates

Contact