Merge pull request #41 from d5/sandbox

sandbox environment for scripts
This commit is contained in:
Daniel Kang 2019-01-24 14:00:44 -08:00 committed by GitHub
commit 1315489a4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 134 additions and 14 deletions

View file

@ -4,9 +4,9 @@
- [Using Scripts](#using-scripts) - [Using Scripts](#using-scripts)
- [Type Conversion Table](#type-conversion-table) - [Type Conversion Table](#type-conversion-table)
- [Compiler and VM](#compiler-and-vm)
- [User Types](#user-types) - [User Types](#user-types)
- [Sandbox Environments](#sandbox-environments) - [Sandbox Environments](#sandbox-environments)
- [Compiler and VM](#compiler-and-vm)
## Using Scripts ## Using Scripts
@ -102,15 +102,52 @@ When adding a Variable _([Script.Add](https://godoc.org/github.com/d5/tengo/scri
One can easily add and use customized value types in Tengo code by implementing [Object](https://godoc.org/github.com/d5/tengo/objects#Object) interface. Tengo runtime will treat the user types exactly in the same way it does to the runtime types with no performance overhead. See [Tengo Objects](https://github.com/d5/tengo/blob/master/docs/objects.md) for more details. One can easily add and use customized value types in Tengo code by implementing [Object](https://godoc.org/github.com/d5/tengo/objects#Object) interface. Tengo runtime will treat the user types exactly in the same way it does to the runtime types with no performance overhead. See [Tengo Objects](https://github.com/d5/tengo/blob/master/docs/objects.md) for more details.
## Sandbox Environments
To securely compile and execute _potentially_ unsafe script code, you can use the following Script functions.
#### Script.DisableBuiltinFunction(name string)
DisableBuiltinFunction disables and removes a builtin function from the compiler. Compiler will reports a compile-time error if the given name is referenced.
```golang
s := script.New([]byte(`print([1, 2, 3])`))
s.DisableBuiltinFunction("print")
_, err := s.Run() // compile error
```
#### Script.DisableStdModule(name string)
DisableStdModule disables a [standard library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) module. Compile will report a compile-time error if the code tries to import the module with the given name.
```golang
s := script.New([]byte(`import("exec")`))
s.DisableStdModule("exec")
_, err := s.Run() // compile error
```
#### Script.SetUserModuleLoader(loader compiler.ModuleLoader)
SetUserModuleLoader replaces the default user-module loader of the compiler, which tries to read the source from a local file.
```golang
s := script.New([]byte(`math := import("mod1"); a := math.foo()`))
s.SetUserModuleLoader(func(moduleName string) ([]byte, error) {
if moduleName == "mod1" {
return []byte(`foo := func() { return 5 }`), nil
}
return nil, errors.New("module not found")
})
```
## Compiler and VM ## Compiler and VM
Although it's not recommended, you can directly create and run the Tengo [Parser](https://godoc.org/github.com/d5/tengo/compiler/parser#Parser), [Compiler](https://godoc.org/github.com/d5/tengo/compiler#Compiler), and [VM](https://godoc.org/github.com/d5/tengo/runtime#VM) for yourself instead of using Scripts and Script Variables. It's a bit more involved as you have to manage the symbol tables and global variables between them, but, basically that's what Script and Script Variable is doing internally. Although it's not recommended, you can directly create and run the Tengo [Parser](https://godoc.org/github.com/d5/tengo/compiler/parser#Parser), [Compiler](https://godoc.org/github.com/d5/tengo/compiler#Compiler), and [VM](https://godoc.org/github.com/d5/tengo/runtime#VM) for yourself instead of using Scripts and Script Variables. It's a bit more involved as you have to manage the symbol tables and global variables between them, but, basically that's what Script and Script Variable is doing internally.
_TODO: add more information here_ _TODO: add more information here_
## Sandbox Environments
In an environment where a _(potentially)_ unsafe script code needs to be executed,
_TODO: add more information here_

View file

@ -7,14 +7,18 @@ import (
"github.com/d5/tengo/compiler" "github.com/d5/tengo/compiler"
"github.com/d5/tengo/compiler/parser" "github.com/d5/tengo/compiler/parser"
"github.com/d5/tengo/compiler/source" "github.com/d5/tengo/compiler/source"
"github.com/d5/tengo/compiler/stdlib"
"github.com/d5/tengo/objects" "github.com/d5/tengo/objects"
"github.com/d5/tengo/runtime" "github.com/d5/tengo/runtime"
) )
// Script can simplify compilation and execution of embedded scripts. // Script can simplify compilation and execution of embedded scripts.
type Script struct { type Script struct {
variables map[string]*Variable variables map[string]*Variable
input []byte removedBuiltins map[string]bool
removedStdModules map[string]bool
userModuleLoader compiler.ModuleLoader
input []byte
} }
// New creates a Script instance with an input script. // New creates a Script instance with an input script.
@ -52,9 +56,32 @@ func (s *Script) Remove(name string) bool {
return true return true
} }
// DisableBuiltinFunction disables a builtin function.
func (s *Script) DisableBuiltinFunction(name string) {
if s.removedBuiltins == nil {
s.removedBuiltins = make(map[string]bool)
}
s.removedBuiltins[name] = true
}
// DisableStdModule disables a standard library module.
func (s *Script) DisableStdModule(name string) {
if s.removedStdModules == nil {
s.removedStdModules = make(map[string]bool)
}
s.removedStdModules[name] = true
}
// SetUserModuleLoader sets the user module loader for the compiler.
func (s *Script) SetUserModuleLoader(loader compiler.ModuleLoader) {
s.userModuleLoader = loader
}
// Compile compiles the script with all the defined variables, and, returns Compiled object. // Compile compiles the script with all the defined variables, and, returns Compiled object.
func (s *Script) Compile() (*Compiled, error) { func (s *Script) Compile() (*Compiled, error) {
symbolTable, globals := s.prepCompile() symbolTable, stdModules, globals := s.prepCompile()
fileSet := source.NewFileSet() fileSet := source.NewFileSet()
@ -64,7 +91,12 @@ func (s *Script) Compile() (*Compiled, error) {
return nil, fmt.Errorf("parse error: %s", err.Error()) return nil, fmt.Errorf("parse error: %s", err.Error())
} }
c := compiler.NewCompiler(symbolTable, nil, nil) c := compiler.NewCompiler(symbolTable, stdModules, nil)
if s.userModuleLoader != nil {
c.SetModuleLoader(s.userModuleLoader)
}
if err := c.Compile(file); err != nil { if err := c.Compile(file); err != nil {
return nil, err return nil, err
} }
@ -100,7 +132,7 @@ func (s *Script) RunContext(ctx context.Context) (compiled *Compiled, err error)
return return
} }
func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, globals []*objects.Object) { func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, stdModules map[string]*objects.ImmutableMap, globals []*objects.Object) {
var names []string var names []string
for name := range s.variables { for name := range s.variables {
names = append(names, name) names = append(names, name)
@ -108,7 +140,16 @@ func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, globals []*ob
symbolTable = compiler.NewSymbolTable() symbolTable = compiler.NewSymbolTable()
for idx, fn := range objects.Builtins { for idx, fn := range objects.Builtins {
symbolTable.DefineBuiltin(idx, fn.Name) if !s.removedBuiltins[fn.Name] {
symbolTable.DefineBuiltin(idx, fn.Name)
}
}
stdModules = make(map[string]*objects.ImmutableMap)
for name, mod := range stdlib.Modules {
if !s.removedStdModules[name] {
stdModules[name] = mod
}
} }
globals = make([]*objects.Object, len(names), len(names)) globals = make([]*objects.Object, len(names), len(names))

View file

@ -1,6 +1,7 @@
package script_test package script_test
import ( import (
"errors"
"testing" "testing"
"github.com/d5/tengo/assert" "github.com/d5/tengo/assert"
@ -36,3 +37,44 @@ func TestScript_Run(t *testing.T) {
assert.NotNil(t, c) assert.NotNil(t, c)
compiledGet(t, c, "a", int64(5)) compiledGet(t, c, "a", int64(5))
} }
func TestScript_DisableBuiltinFunction(t *testing.T) {
s := script.New([]byte(`a := len([1, 2, 3])`))
c, err := s.Run()
assert.NoError(t, err)
assert.NotNil(t, c)
compiledGet(t, c, "a", int64(3))
s.DisableBuiltinFunction("len")
_, err = s.Run()
assert.Error(t, err)
}
func TestScript_DisableStdModule(t *testing.T) {
s := script.New([]byte(`math := import("math"); a := math.abs(-19.84)`))
c, err := s.Run()
assert.NoError(t, err)
assert.NotNil(t, c)
compiledGet(t, c, "a", 19.84)
s.DisableStdModule("math")
_, err = s.Run()
assert.Error(t, err)
}
func TestScript_SetUserModuleLoader(t *testing.T) {
s := script.New([]byte(`math := import("mod1"); a := math.foo()`))
_, err := s.Run()
assert.Error(t, err)
s.SetUserModuleLoader(func(moduleName string) (res []byte, err error) {
if moduleName == "mod1" {
res = []byte(`foo := func() { return 5 }`)
return
}
err = errors.New("module not found")
return
})
c, err := s.Run()
assert.NoError(t, err)
assert.NotNil(t, c)
compiledGet(t, c, "a", int64(5))
}