Gopherscript
Gopherscript is a secure scripting/configuration language written in Go. It features a fined-grain permission system and enforces a strong isolation of dependencies. Gopherscript is not production ready yet : if you find a bug or want to suggest a feature create an issue please ! Expect breaking changes.
Join the official community on Revolt.
Security & Minimalism
- The codebase is small on purpose (a single Go file with less than 8K lines and only std lib dependencies). See Implementation.
- The default global scope has ZERO variables/functions and there are no "builtin modules" to import. (only add what you need from Golang)
- A strict but extensive permission system allows you to precisely control what is allowed (almost no permissions by default). For more details go to the permission section.
- Paths, path patterns, URLs are literals and dynamic paths are only possible as path expressions. You cannot create them from strings at runtime ! That facilitates the listing of permissions and helps static analysis.
- Properties cannot be accessed with a dynamic name (
$obj[$name]
), only Go functions that are passed objects in can (you have to trust them anyway). - In a general way some features will be added (or refused) to limit code obfuscation.
If you find Gopherscript too restrictive don't worry ! A lax
mode might be introduced in the future.
Have a look at the features section and the wiki !
Installation & Usage
You can use the gos
executable to execute scripts or launch a REPL:
go install github.com/debloat-dev/Gopherscript/cmd/[email protected]
See the documentation here. You can also use Gopherscript as a library and only add whay you need to the global scope (see the example further below).
Editor support
If you use VSCode you can install the extension of ID xbsd.gopherscript
. If you are a NeoVim user, check this repo please.
Example
Example of using Gopherscript as a library from Golang:
package main
import (
gos "github.com/debloat-dev/Gopherscript"
"log"
)
type User struct {
Name string
}
func main() {
//we create a Context that contains the granted permissions
grantedPerms := []gos.Permission{
gos.GlobalVarPermission{gos.UsePerm, "*"},
}
ctx := gos.NewContext(grantedPerms, nil)
//we create the initial state with the globals we want to expose
//the state can be re used several times (and with different modules)
state := gos.NewState(ctx, map[string]interface{}{
"makeUser": func(ctx *gos.Context) User {
return User{Name: "Bar"}
},
})
mod, err := gos.ParseAndCheckModule(`
# permissions must be requested at the top of the file AND granted
require {
use: {globals: "*"}
}
a = 1
user = makeUser()
return [
($a + 2),
$user.Name
]
`, "")
if err != nil {
log.Panicln(err)
}
//we execute the script
res, err := gos.Eval(mod, state)
if err != nil {
log.Panicln(err)
}
log.Printf("%#v", res)
}
You can learn more about the interactions between Gopherscript and Golang here.
Features
The most important features are described in this section. If you want to learn Gopherscript or want to know more details about specific features you can go on the wiki.
Permissions
Required permissions are specified at the top of each module (file).
There are several permission kinds: Create, Update, Read, Delete, Use, Consume, Provide. Some permission types are already provided: FilesystemPermission, HttpPermission, StackPermission, GlobalVarPermission. You can specify your own permissions by implementing the Permission interface (Golang).
type Permission interface {
Kind() PermissionKind
Includes(Permission) bool
}
Gopherscript also provides limitations to prevent a script to take all resources. In the example below writing to the filesystem is limited to 100 kB/s.
require {
[...]
limits: {
"fs/write": 100kB/s
}
}
Special literals & expressions
# Path literals
/home/user/
./file.json
# Path expressions
/home/user/$dirpath$
# Path patterns support basic globbing (*, [set], ?) and prefixing (not both at the same time).
./data/*.json
/app/logs/...
/app/*/... # invalid (this might be allowed in the future though)
# HTTP host literals
https://example.com
https://example.com:443
# HTTP host pattern literals
https://* # any HTTPS host
https://*.com # any domain with .com as TLD, will not match against subdomains
https://*.example.com # any subdomain of example.com
# URL literals
https://example.com/
https://example.com/index.html
https://localhost/
# URL expressions
https://example.com/users/$id$
# URL pattern literals (only prefix patterns supported)
https://example.com/users/...
Quantity literals
10s # time.Duration
10ms # time.Duration
10% # 0.10
sleep 100ms
Imports
Syntax:
import <varname> <url> <file-content-sha256> { <global variables> } allow { <permission> }
Importing a module is like executing a script with the passed globals and granted permissions.
# content of https://example.com/return_1.gos
return 1
Script:
Routines
Routines are mainly used for concurrent work and isolation. Each routine has its own goroutine and state.
Syntax for spawning routines:
$routine = sr [group] <globals> <module | call | variable>
Call (all permissions are inherited).
$routine = sr nil f()
Embedded module:
You can wait for the routine's result by calling the WaitResult method:
$result = $routine.WaitResult()!
Routines can optionally be part of a "routine group" that allows easier control of multiple routines. The group variable is defined (and updated) when the spawn expression is evaluated.
for (1 .. 10) {
sr req_group nil read(https://debloat.dev/fakeapp/users)!
}
$results = $req_group.WaitAllResults()!
For more details about the different features you can read the repository's wiki.
Implementation
- Why use a tree walk interpreter instead of a bytecode interpreter ?
-> Tree walk interpreters are slower but simpler : that means less bugs and vulnerabilities. A Gopherscript implementation that uses a bytecode interpreter might be developed in the future though.
Executables
When something tries to do too many things it becomes impossible to understand for the average developper. That makes audits and customization harder. Goperscript aims to have a different model. Depending on the features you need, you install one one more executables that try to do one thing well without uncessary bloat (each one providing specific globals to Gophercript).