Skip to content
Closed
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ by a tier, not even `full`. For a one-flag "trust everything" run there is
command execution of any command in a single switch. Use it only with fully
trusted scripts.

SQLite's `register_function` is disabled unless the operator passes
`--dangerously-allow-all`. The `open`/`full` tiers and `--allow-cmd` do not enable
custom SQL callbacks. Ordinary SQL and built-in functions remain available.
Restricted connections also exclude callbacks registered earlier by a trusted
box in the same process. This intentionally tightens the previous default.
Custom callbacks run outside the Machine's step budget; use the trusted-script
flag only when that limitation is acceptable.

Set a stricter default for a whole deployment with the env var:

```bash
Expand Down
6 changes: 6 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ do not provide process or memory isolation. The default `open` tier permits
network and filesystem access. `--allow-cmd` authorizes arbitrary host commands
and process environment access, including through `gum` and `runtime`.

SQLite custom functions require `--dangerously-allow-all`. Ordinary module and
command grants do not enable them. Normal connections exclude process-wide
callbacks, including ones registered earlier by a trusted box. Fully trusted
callbacks retain the existing separate-thread behavior and are not bounded by
`--max-steps`; this is not an isolated-worker or complete resource budget.

Use the narrowest capability grants and import directories that your script
needs. HTTP execution has request and cooperative execution limits, but these
do not preempt arbitrary Go builtins or impose a complete output-byte budget.
Expand Down
2 changes: 1 addition & 1 deletion cli/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func ParseArgs() *Args {
flag.BoolVar(&args.AllowNet, "allow-net", false, "widen a restrictive tier with network modules (http, net, email, llm)")
flag.BoolVar(&args.AllowFS, "allow-fs", false, "widen a restrictive tier with filesystem modules (file, path)")
flag.BoolVar(&args.AllowCmd, "allow-cmd", false, "allow ANY host command, gum subprocesses, and runtime environment mutation (trusted scripts only)")
flag.BoolVar(&args.Dangerous, "dangerously-allow-all", false, "DANGER: open everything — network + filesystem + host command execution of ANY command. Use only with fully trusted scripts.")
flag.BoolVar(&args.Dangerous, "dangerously-allow-all", false, "DANGER: open network, filesystem, arbitrary host commands, and SQLite custom functions. Fully trusted scripts only.")
flag.BoolVar(&args.Check, "check", false, "syntax/resolve check the script (-c or file) without running it")
flag.StringVar(&args.LogFile, "log-file", "", "append the script's log module output to this file")
flag.StringVar(&args.LogFormat, "log-format", "console", "log file format: console (human) or json (machine)")
Expand Down
94 changes: 94 additions & 0 deletions cli/capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ package cli
// - wired pure domain modules are classified pure and pass the safe gate
// - cmd execution gating: off by default, on with --allow-cmd /
// --dangerously-allow-all (the execCmd lever + end-to-end run())
// - SQLite custom functions require the fully trusted-script grant

import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"

"github.com/1set/starlet"
Expand Down Expand Up @@ -441,3 +443,95 @@ func TestProcessCapabilityBoundary(t *testing.T) {
}
}
}

// SQLite callbacks use their own Starlark threads and process-wide registry.
// Ordinary module/network/filesystem/command grants must not authorize them.
var sqlitePolicyFunctionID uint64

func TestSQLiteCustomFunctionPolicy(t *testing.T) {
cases := []struct {
name string
opts BoxOpts
allowed bool
}{
{"default open", BoxOpts{}, false},
{"explicit open", BoxOpts{caps: "open"}, false},
{"full tier", BoxOpts{caps: "full"}, false},
{"network and filesystem", BoxOpts{caps: "safe", allowNet: true, allowFS: true}, false},
{"command grant", BoxOpts{allowCmd: true}, false},
{"fully trusted", BoxOpts{caps: "safe", dangerous: true}, true},
}
for _, tc := range cases {
for _, eager := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/eager=%t", tc.name, eager), func(t *testing.T) {
opts := tc.opts
checkSQLiteFunctionPolicy(t, &opts, eager, tc.allowed)
})
}
}
}

func checkSQLiteFunctionPolicy(t *testing.T, opts *BoxOpts, eager, allowed bool) {
t.Helper()
opts.scenario, opts.printerName = scenarioDirect, "none"
opts.moduleToLoad = []string{"sqlite"}
box, err := BuildBox(opts)
if err != nil {
t.Fatal(err)
}
fn := fmt.Sprintf("starcli_policy_fn_%d", atomic.AddUint64(&sqlitePolicyFunctionID, 1))
load, prefix := `load("sqlite", "connect", "register_function")`, ""
if eager {
load, prefix = "", "sqlite."
}
code := fmt.Sprintf(`
%s
%sregister_function(%q, lambda: 73, num_args=0)
db = %sconnect(":memory:")
def verify():
if db.query_one("SELECT %s() AS result")["result"] != 73:
fail("custom function returned an incorrect value")
verify()
db.close()
`, load, prefix, fn, prefix, fn)
_, err = box.Run(code)
if allowed {
if err != nil {
t.Fatal(err)
}
return
}
if err == nil || !strings.Contains(err.Error(), "custom functions are disabled by the host") {
t.Fatalf("registration must be rejected by host policy, got %v", err)
}
}

func TestSQLiteRestrictedConnectionIgnoresTrustedFunctions(t *testing.T) {
trusted, err := BuildBox(&BoxOpts{scenario: scenarioDirect, printerName: "none", moduleToLoad: []string{"sqlite"}, dangerous: true})
if err != nil {
t.Fatal(err)
}
fn := fmt.Sprintf("starcli_private_fn_%d", atomic.AddUint64(&sqlitePolicyFunctionID, 1))
if _, err = trusted.Run(fmt.Sprintf(`load("sqlite", "register_function"); register_function(%q, lambda: 41, num_args=0)`, fn)); err != nil {
t.Fatal(err)
}
restricted, err := BuildBox(&BoxOpts{scenario: scenarioDirect, printerName: "none", moduleToLoad: []string{"sqlite"}, caps: "full"})
if err != nil {
t.Fatal(err)
}
_, err = restricted.Run(fmt.Sprintf(`load("sqlite", "connect"); db = connect(":memory:"); row = db.query_one("SELECT %s() AS result")`, fn))
if err == nil || !strings.Contains(err.Error(), "no such function") {
t.Fatalf("restricted connection inherited a process-wide callback: %v", err)
}
if _, err = restricted.Run(`
load("sqlite", "connect")
db = connect(":memory:")
def verify():
if db.query_one("SELECT abs(-73) AS result")["result"] != 73:
fail("built-in SQLite functions must remain usable")
verify()
db.close()
`); err != nil {
t.Fatalf("ordinary SQL on a restricted connection: %v", err)
}
}
7 changes: 6 additions & 1 deletion cli/mods.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ func loadCLIModuleByName(opts *BoxOpts, name string) (starlet.ModuleLoader, erro
}
return cmd.NewModule().LoadModule(), nil
case sqlite.ModuleName:
return sqlite.NewModule().LoadModule(), nil
// SQLite callbacks use separate Starlark threads and a process-wide
// registry. Only the fully trusted-script grant may enable them; ordinary
// SQL and built-in functions remain available under the normal grant.
return sqlite.NewModuleWithHostPolicy(sqlite.HostPolicy{
DisableCustomFunctions: !opts.dangerous,
}).LoadModule(), nil
case web.ModuleName:
return web.NewModule().LoadModule(), nil
case cache.ModuleName:
Expand Down
36 changes: 35 additions & 1 deletion e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// *work* through the CLI, not merely that they load.
//
// Sections: local imports, golden scripts, domain modules, stdin, recordings,
// containers.
// containers, SQLite custom-function authorization.
package e2e

import (
Expand Down Expand Up @@ -378,3 +378,37 @@ func TestContainer(t *testing.T) {
}
})
}

// TestSQLiteCustomFunctionAuthorization exercises real argument parsing and the
// production module registry, including SQL calls after the explicit opt-in.
func TestSQLiteCustomFunctionAuthorization(t *testing.T) {
const code = `
load("sqlite", "connect", "register_function")
register_function("starcli_e2e_answer", lambda: 73, num_args=0)
db = connect(":memory:")
print(db.query_one("SELECT starcli_e2e_answer() AS result")["result"])
db.close()
`
for _, tc := range []struct {
name string
flags []string
allowed bool
}{
{"default", nil, false},
{"full tier", []string{"--caps", "full"}, false},
{"command grant", []string{"--allow-cmd"}, false},
{"fully trusted", []string{"--dangerously-allow-all"}, true},
} {
t.Run(tc.name, func(t *testing.T) {
args := append(append([]string{}, tc.flags...), "-o", "stdout", "-c", code)
stdout, stderr, exit := runCLI(t, "", args...)
if tc.allowed {
if exit != 0 || !strings.Contains(stdout, "73") {
t.Fatalf("trusted custom function: exit=%d stdout=%q stderr=%q", exit, stdout, stderr)
}
} else if exit == 0 || !strings.Contains(stderr, "custom functions are disabled by the host") {
t.Fatalf("custom function requires the trusted-script grant: exit=%d stdout=%q stderr=%q", exit, stdout, stderr)
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/starpkg/llm v0.1.0
github.com/starpkg/markdown v0.1.0
github.com/starpkg/qrcode v0.1.0
github.com/starpkg/sqlite v0.2.1
github.com/starpkg/sqlite v0.2.2-0.20260923164825-56739df72ba2
github.com/starpkg/toml v0.1.0
github.com/starpkg/totp v0.1.0
github.com/starpkg/web v0.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ github.com/starpkg/markdown v0.1.0 h1:buLZwTcZ9SQWrVOUVeTcHqw/DCkxKPE8rdvHpbhjrE
github.com/starpkg/markdown v0.1.0/go.mod h1:JsRNXRFjvLPRrkjGrf6jXDTl3Qg9+d2/PByzW7WxYvg=
github.com/starpkg/qrcode v0.1.0 h1:VULD2XBVacMq0c2WxJgdG0d3k0Z4EOSaFdYdkOB7bME=
github.com/starpkg/qrcode v0.1.0/go.mod h1:itKiBubTuioyMM78bA+RTfwm/hH3EYG+W6NX0cscgh0=
github.com/starpkg/sqlite v0.2.1 h1:oTVkPCTQYhud3qDXztJsb/1cBY4SgXLF2PKonRhJPFI=
github.com/starpkg/sqlite v0.2.1/go.mod h1:Hfs3JZnFbDPefCL9ybSmRJIQ+IBVMYtj5FyZ+7XZpmc=
github.com/starpkg/sqlite v0.2.2-0.20260923164825-56739df72ba2 h1:YS+in+4QZRDTboQWFip0HFcduKdFV3xDIEMKTq/9IDc=
github.com/starpkg/sqlite v0.2.2-0.20260923164825-56739df72ba2/go.mod h1:16YgyyCzRaKMDqWZO6uucWIKyG3wWuIJx/O9MsDKHDs=
github.com/starpkg/toml v0.1.0 h1:geXbLw/SW7XcU9Tz/eqjxSNywsMbxqfndXYOlXXkSKE=
github.com/starpkg/toml v0.1.0/go.mod h1:8QtTjIjFNuw0WGYzDHFbynhQ21QM6BvdACzapKE1cr8=
github.com/starpkg/totp v0.1.0 h1:BQ1J5bbhR8iICQ+iRJ4LZgJNX3RhhOZtY2aoaf38WqY=
Expand Down
Loading