Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ Every access path is default-deny:

| Resource | Default | Opt-in |
|----------------------|-------------------------------------|----------------------------------------------|
| Command execution | All commands blocked (exit code 127)| `AllowedCommands` with namespaced command list (e.g. `rshell:cat`), or `AllowAllCommands` |
| Command execution | All commands blocked (exit code 127)| `AllowedCommands` with namespaced command list (e.g. `rshell:cat`) |
| External commands | Blocked (exit code 127) | Provide an `ExecHandler` |
| Filesystem access | Blocked | Configure `AllowedPaths` with directory list |
| Environment variables| Empty (no host env inherited) | Pass variables via the `Env` option |
| Output redirections | Only `/dev/null` allowed (exit code 2 for other targets) | `>/dev/null`, `2>/dev/null`, `&>/dev/null`, `2>&1` |

**AllowedCommands** restricts which commands (builtins or external) the interpreter may execute. Commands must be specified with the `rshell:` namespace prefix (e.g. `rshell:cat`, `rshell:echo`). If not set, no commands are allowed. Use `AllowAllCommands` to permit all commands (useful for testing).
**AllowedCommands** restricts which commands (builtins or external) the interpreter may execute. Commands must be specified with the `rshell:` namespace prefix (e.g. `rshell:cat`, `rshell:echo`). If not set, no commands are allowed.

**AllowedPaths** restricts all file operations to specified directories using Go's `os.Root` API (`openat` syscalls), making it immune to symlink traversal, TOCTOU races, and `..` escape attacks.

Expand Down
1 change: 0 additions & 1 deletion SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ Blocked features are rejected before execution with exit code 2.
## Execution

- ✅ AllowedCommands — restricts which commands (builtins or external) may be executed; commands require the `rshell:` namespace prefix (e.g. `rshell:cat`); if not set, no commands are allowed
- ✅ AllowAllCommands — permits any command (testing convenience)
- ✅ AllowedPaths filesystem sandboxing — restricts all file access to specified directories
- ✅ Whole-run execution timeout — callers can bound a `Run()` call via `context.Context`, `interp.MaxExecutionTime`, or the CLI `--timeout` flag; the deadline applies to the entire script, not each individual command
- ✅ ProcPath — overrides the proc filesystem path used by `ps` (default `/proc`; Linux-only; useful for testing/container environments)
Expand Down
3 changes: 2 additions & 1 deletion builtins/ps/ps_procpath_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/stretchr/testify/require"
"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -34,7 +35,7 @@ func runScriptWithProcPath(t *testing.T, script, procPath string) (stdout, stder
var outBuf, errBuf bytes.Buffer
runner, err := interp.New(
interp.StdIO(nil, &outBuf, &errBuf),
interp.AllowAllCommands(),
interpoption.AllowAllCommands().(interp.RunnerOption),
interp.ProcPath(procPath),
)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion builtins/ps/ps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -27,7 +28,7 @@ func runScript(t *testing.T, script string) (stdout, stderr string, code int) {
t.Fatal(err)
}
var outBuf, errBuf bytes.Buffer
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands())
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption))
if err != nil {
t.Fatal(err)
}
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/cat/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
t.Fatal(err)
}
var outBuf, errBuf bytes.Buffer
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...)
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...)
runner, err := interp.New(allOpts...)
if err != nil {
t.Fatal(err)
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/cut/cut_gnu_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/require"
"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -31,7 +32,7 @@ func cutRun(t *testing.T, script, dir string) (string, string, int) {
opts := []interp.RunnerOption{
interp.StdIO(nil, &outBuf, &errBuf),
interp.AllowedPaths([]string{dir}),
interp.AllowAllCommands(),
interpoption.AllowAllCommands().(interp.RunnerOption),
}

runner, err := interp.New(opts...)
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/cut/cut_pentest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/require"
"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -37,7 +38,7 @@ func cutPentestRunCtx(ctx context.Context, t *testing.T, script, dir string) (st
opts := []interp.RunnerOption{
interp.StdIO(nil, &outBuf, &errBuf),
interp.AllowedPaths([]string{dir}),
interp.AllowAllCommands(),
interpoption.AllowAllCommands().(interp.RunnerOption),
}

runner, err := interp.New(opts...)
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/cut/cut_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/require"
"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -32,7 +33,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
prog, err := parser.Parse(strings.NewReader(script), "")
require.NoError(t, err)
var outBuf, errBuf bytes.Buffer
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...)
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...)
runner, err := interp.New(allOpts...)
require.NoError(t, err)
defer runner.Close()
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/head/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
t.Fatal(err)
}
var outBuf, errBuf bytes.Buffer
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...)
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...)
runner, err := interp.New(allOpts...)
if err != nil {
t.Fatal(err)
Expand Down
31 changes: 16 additions & 15 deletions builtins/tests/help/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/require"
"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand Down Expand Up @@ -53,7 +54,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
// --- Exit code ---

func TestHelpExitCode(t *testing.T) {
stdout, stderr, code := runScript(t, "help", "", interp.AllowAllCommands())
stdout, stderr, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)
assert.Empty(t, stderr)
assert.NotEmpty(t, stdout)
Expand All @@ -62,7 +63,7 @@ func TestHelpExitCode(t *testing.T) {
// --- Output content ---

func TestHelpListsAllBuiltins(t *testing.T) {
stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)

// Every registered builtin should appear in the output.
Expand All @@ -78,7 +79,7 @@ func TestHelpListsAllBuiltins(t *testing.T) {
}

func TestHelpListsSorted(t *testing.T) {
stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)

lines := strings.Split(strings.TrimSpace(stdout), "\n")
Expand Down Expand Up @@ -108,7 +109,7 @@ func TestHelpListsSorted(t *testing.T) {
}

func TestHelpIncludesDescriptions(t *testing.T) {
stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)

// Spot-check a few descriptions.
Expand All @@ -119,13 +120,13 @@ func TestHelpIncludesDescriptions(t *testing.T) {
}

func TestHelpIncludesFooterHint(t *testing.T) {
stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)
assert.Contains(t, stdout, "Run 'help <command>' for more information on a specific command.")
}

func TestHelpColumnsAligned(t *testing.T) {
stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)

lines := strings.Split(strings.TrimSpace(stdout), "\n")
Expand Down Expand Up @@ -235,56 +236,56 @@ func TestHelpAlwaysAvailableNoCommands(t *testing.T) {
// --- Error handling ---

func TestHelpUnknownCommandShowsError(t *testing.T) {
_, stderr, code := runScript(t, "help foo", "", interp.AllowAllCommands())
_, stderr, code := runScript(t, "help foo", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "no help topics match 'foo'")
}

func TestHelpShowsCommandHelp(t *testing.T) {
stdout, _, code := runScript(t, "help echo", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help echo", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)
assert.Contains(t, stdout, "echo: echo [-neE]")
}

func TestHelpFlagPrintsUsage(t *testing.T) {
stdout, _, code := runScript(t, "help --help", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help --help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 1, code)
assert.Contains(t, stdout, "Usage: help")
assert.Contains(t, stdout, "Display help for builtin commands.")
}

func TestHelpUnknownFlagRejected(t *testing.T) {
_, stderr, code := runScript(t, "help --verbose", "", interp.AllowAllCommands())
_, stderr, code := runScript(t, "help --verbose", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 1, code)
assert.Contains(t, stderr, "help:")
}

// --- Pipeline / composition ---

func TestHelpInPipeline(t *testing.T) {
stdout, _, code := runScript(t, "help | grep echo", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help | grep echo", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)
assert.Contains(t, stdout, "echo")
assert.Contains(t, stdout, "write arguments to stdout")
}

func TestHelpExitCodeInScript(t *testing.T) {
stdout, _, code := runScript(t, "help; echo $?", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help; echo $?", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)
// The last line before the footer should be "0" from echo $?.
assert.True(t, strings.HasSuffix(strings.TrimSpace(stdout), "0"))
}

func TestHelpFailExitCodeInScript(t *testing.T) {
stdout, _, code := runScript(t, "help badarg; echo $?", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help badarg; echo $?", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code) // overall script exits 0 because echo $? succeeds
assert.True(t, strings.HasSuffix(strings.TrimSpace(stdout), "1"))
}

// --- Help lists itself ---

func TestHelpListsItself(t *testing.T) {
stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands())
stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)
assert.Contains(t, stdout, "help")
assert.Contains(t, stdout, "display help for commands")
Expand All @@ -293,7 +294,7 @@ func TestHelpListsItself(t *testing.T) {
// --- Empty stderr on success ---

func TestHelpNoStderrOnSuccess(t *testing.T) {
_, stderr, code := runScript(t, "help", "", interp.AllowAllCommands())
_, stderr, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption))
assert.Equal(t, 0, code)
assert.Empty(t, stderr)
}
3 changes: 2 additions & 1 deletion builtins/tests/ip/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -31,7 +32,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
t.Fatal(err)
}
var outBuf, errBuf bytes.Buffer
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...)
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...)
runner, err := interp.New(allOpts...)
if err != nil {
t.Fatal(err)
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/ip/ip_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -44,7 +45,7 @@ func cmdRunCtxFuzz(ctx context.Context, t *testing.T, script string) (stdout, st
return "", err.Error(), -1
}
var outBuf, errBuf bytes.Buffer
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands())
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption))
if err != nil {
t.Fatalf("interp.New: %v", err)
}
Expand Down
7 changes: 4 additions & 3 deletions builtins/tests/ps/ps_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -27,7 +28,7 @@ func runPS(t testing.TB, script string) (string, string, int) {
return "", err.Error(), 1
}
var outBuf, errBuf bytes.Buffer
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands())
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption))
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -86,7 +87,7 @@ func FuzzPSPidList(f *testing.F) {
return // unparseable, skip
}
var outBuf, errBuf bytes.Buffer
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands())
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption))
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -133,7 +134,7 @@ func FuzzPSFlags(f *testing.F) {
return
}
var outBuf, errBuf bytes.Buffer
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands())
runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption))
if err != nil {
t.Fatal(err)
}
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/sed/sed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"testing"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -31,7 +32,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
prog, err := parser.Parse(strings.NewReader(script), "")
require.NoError(t, err)
var outBuf, errBuf bytes.Buffer
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...)
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...)
runner, err := interp.New(allOpts...)
require.NoError(t, err)
defer runner.Close()
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/tail/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
t.Fatal(err)
}
var outBuf, errBuf bytes.Buffer
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...)
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...)
runner, err := interp.New(allOpts...)
if err != nil {
t.Fatal(err)
Expand Down
3 changes: 2 additions & 1 deletion builtins/tests/wc/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"mvdan.cc/sh/v3/syntax"

"github.com/DataDog/rshell/internal/interpoption"
"github.com/DataDog/rshell/interp"
)

Expand All @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts ..
t.Fatal(err)
}
var outBuf, errBuf bytes.Buffer
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...)
allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...)
runner, err := interp.New(allOpts...)
if err != nil {
t.Fatal(err)
Expand Down
Loading
Loading