Skip to content

Commit

Permalink
interp: virtualize environment in restricted mode
Browse files Browse the repository at this point in the history
In restricted mode, replace environment related symbols in
stdlib os package by a version which operates on a private copy
per interpreter context.

It allows to have concurrent interpreters in the same process
operating each in their own environment without affecting each
other or the host.

If unrestricted opt is set, this behaviour is disabled, and the
default symbols from stdlib are used.

Note also that no modification is done for syscall package, as it
should be not used in restricted mode.
  • Loading branch information
mvertes committed Nov 8, 2021
1 parent afa46da commit a876bb3
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 14 deletions.
7 changes: 6 additions & 1 deletion cmd/yaegi/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ func run(arg []string) error {
}
args := rflag.Args()

i := interp.New(interp.Options{GoPath: build.Default.GOPATH, BuildTags: strings.Split(tags, ",")})
i := interp.New(interp.Options{
GoPath: build.Default.GOPATH,
BuildTags: strings.Split(tags, ","),
Env: os.Environ(),
Unrestricted: useUnrestricted,
})
if err := i.Use(stdlib.Symbols); err != nil {
return err
}
Expand Down
7 changes: 6 additions & 1 deletion cmd/yaegi/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ func test(arg []string) (err error) {
return err
}

i := interp.New(interp.Options{GoPath: build.Default.GOPATH, BuildTags: strings.Split(tags, ",")})
i := interp.New(interp.Options{
GoPath: build.Default.GOPATH,
BuildTags: strings.Split(tags, ","),
Env: os.Environ(),
Unrestricted: useUnrestricted,
})
if err := i.Use(stdlib.Symbols); err != nil {
return err
}
Expand Down
62 changes: 50 additions & 12 deletions interp/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,20 +169,22 @@ type imports map[string]map[string]*symbol

// opt stores interpreter options.
type opt struct {
astDot bool // display AST graph (debug)
cfgDot bool // display CFG graph (debug)
// dotCmd is the command to process the dot graph produced when astDot and/or
// cfgDot is enabled. It defaults to 'dot -Tdot -o <filename>.dot'.
dotCmd string
noRun bool // compile, but do not run
fastChan bool // disable cancellable chan operations
context build.Context // build context: GOPATH, build constraints
specialStdio bool // Allows os.Stdin, os.Stdout, os.Stderr to not be file descriptors
stdin io.Reader // standard input
stdout io.Writer // standard output
stderr io.Writer // standard error
args []string // cmdline args
filesystem fs.FS
context build.Context // build context: GOPATH, build constraints
stdin io.Reader // standard input
stdout io.Writer // standard output
stderr io.Writer // standard error
args []string // cmdline args
env map[string]string // environment of interpreter, entries in form of "key=value"
filesystem fs.FS // filesystem containing sources
astDot bool // display AST graph (debug)
cfgDot bool // display CFG graph (debug)
noRun bool // compile, but do not run
fastChan bool // disable cancellable chan operations
specialStdio bool // allows os.Stdin, os.Stdout, os.Stderr to not be file descriptors
unrestricted bool // allow use of non sandboxed symbols
}

// Interpreter contains global resources and state.
Expand Down Expand Up @@ -307,17 +309,23 @@ type Options struct {
// Cmdline args, defaults to os.Args.
Args []string

// Environment of interpreter. Entries are in the form "key=values".
Env []string

// SourcecodeFilesystem is where the _sourcecode_ is loaded from and does
// NOT affect the filesystem of scripts when they run.
// It can be any fs.FS compliant filesystem (e.g. embed.FS, or fstest.MapFS for testing)
// See example/fs/fs_test.go for an example.
SourcecodeFilesystem fs.FS

// Unrestricted allows to run non sandboxed stdlib symbols such as os/exec and environment
Unrestricted bool
}

// New returns a new interpreter.
func New(options Options) *Interpreter {
i := Interpreter{
opt: opt{context: build.Default, filesystem: &realFS{}},
opt: opt{context: build.Default, filesystem: &realFS{}, env: map[string]string{}},
frame: newFrame(nil, 0, 0),
fset: token.NewFileSet(),
universe: initUniverse(),
Expand Down Expand Up @@ -345,6 +353,20 @@ func New(options Options) *Interpreter {
i.opt.args = os.Args
}

// unrestricted allows to use non sandboxed stdlib symbols and env.
if options.Unrestricted {
i.opt.unrestricted = true
} else {
for _, e := range options.Env {
a := strings.SplitN(e, "=", 2)
if len(a) == 2 {
i.opt.env[a[0]] = a[1]
} else {
i.opt.env[a[0]] = ""
}
}
}

if options.SourcecodeFilesystem != nil {
i.opt.filesystem = options.SourcecodeFilesystem
}
Expand Down Expand Up @@ -742,6 +764,22 @@ func fixStdlib(interp *Interpreter) {
p["Stderr"] = reflect.ValueOf(&s).Elem()
}
}
if !interp.unrestricted {
// In restricted mode, scripts can only access to a passed virtualized env, and can not write the real one.
getenv := func(key string) string { return interp.env[key] }
p["Clearenv"] = reflect.ValueOf(func() { interp.env = map[string]string{} })
p["ExpandEnv"] = reflect.ValueOf(func(s string) string { return os.Expand(s, getenv) })
p["Getenv"] = reflect.ValueOf(getenv)
p["LookupEnv"] = reflect.ValueOf(func(key string) (s string, ok bool) { s, ok = interp.env[key]; return })
p["Setenv"] = reflect.ValueOf(func(key, value string) error { interp.env[key] = value; return nil })
p["Unsetenv"] = reflect.ValueOf(func(key string) error { delete(interp.env, key); return nil })
p["Environ"] = reflect.ValueOf(func() (a []string) {
for k, v := range interp.env {
a = append(a, k+"="+v)
}
return
})
}
}

if p = interp.binPkg["math/bits"]; p != nil {
Expand Down
24 changes: 24 additions & 0 deletions interp/interp_eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1726,3 +1726,27 @@ func TestPassArgs(t *testing.T) {
{src: "os.Args", res: "[arg0 arg1]"},
})
}

func TestRestrictedEnv(t *testing.T) {
i := interp.New(interp.Options{Env: []string{"foo=bar"}})
if err := i.Use(stdlib.Symbols); err != nil {
t.Fatal(err)
}
i.ImportUsed()
runTests(t, i, []testCase{
{src: `os.Getenv("foo")`, res: "bar"},
{src: `s, ok := os.LookupEnv("foo"); s`, res: "bar"},
{src: `s, ok := os.LookupEnv("foo"); ok`, res: "true"},
{src: `s, ok := os.LookupEnv("PATH"); s`, res: ""},
{src: `s, ok := os.LookupEnv("PATH"); ok`, res: "false"},
{src: `os.Setenv("foo", "baz"); os.Environ()`, res: "[foo=baz]"},
{src: `os.ExpandEnv("foo is ${foo}")`, res: "foo is baz"},
{src: `os.Unsetenv("foo"); os.Environ()`, res: "[]"},
{src: `os.Setenv("foo", "baz"); os.Environ()`, res: "[foo=baz]"},
{src: `os.Clearenv(); os.Environ()`, res: "[]"},
{src: `os.Setenv("foo", "baz"); os.Environ()`, res: "[foo=baz]"},
})
if s, ok := os.LookupEnv("foo"); ok {
t.Fatal("expected \"\", got " + s)
}
}

0 comments on commit a876bb3

Please sign in to comment.