From c51248793e53fdb2d457cb668f66426a5fd46210 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Sat, 3 Dec 2022 11:52:09 -0800 Subject: [PATCH] Do two parsing passes when building a package, and prompt user to view script after the first --- build.go | 40 +++++++++++++++++- cli.go | 11 ++--- helpers.go | 5 +++ install.go | 5 --- internal/shutils/exec.go | 29 ++++++++----- internal/shutils/restricted.go | 76 ++++++++++++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 internal/shutils/restricted.go diff --git a/build.go b/build.go index 7b19966..5abb7be 100644 --- a/build.go +++ b/build.go @@ -151,10 +151,18 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st env := genBuildEnv(info) + scriptDir := filepath.Dir(script) + + // The first pass is just used to get variable values and runs before + // the script is displayed, so it is restricted so as to prevent malicious + // code from executing. runner, err := interp.New( interp.Env(expand.ListEnviron(env...)), interp.StdIO(os.Stdin, os.Stdout, os.Stderr), - interp.ExecHandler(helpers.ExecHandler), + interp.ExecHandler(rHelpers.ExecHandler(shutils.RestrictedExec("source"))), + interp.ReadDirHandler(shutils.RestrictedReadDir(scriptDir)), + interp.StatHandler(shutils.RestrictedStat(scriptDir)), + interp.OpenHandler(shutils.RestrictedOpen(scriptDir)), ) if err != nil { return nil, nil, err @@ -179,6 +187,11 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st return nil, nil, err } + err = promptViewScript(script, vars.Name) + if err != nil { + log.Fatal("Failed to prompt user to view build script").Err(err).Send() + } + if !archMatches(vars.Architectures) { buildAnyway, err := yesNoPrompt("Your system's CPU architecture doesn't match this package. Do you want to build anyway?", true) if err != nil { @@ -192,6 +205,31 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send() + // The second pass will be used to execute the actual functions, + // so it cannot be restricted. The script has already been displayed + // to the user by this point, so it should be safe + runner, err = interp.New( + interp.Env(expand.ListEnviron(env...)), + interp.StdIO(os.Stdin, os.Stdout, os.Stderr), + interp.ExecHandler(helpers.ExecHandler(nil)), + ) + if err != nil { + return nil, nil, err + } + + err = runner.Run(ctx, file) + if err != nil { + return nil, nil, err + } + + dec = decoder.New(info, runner) + + // If distro was changed, the list of like distros + // no longer applies, so disable its use + if distroChanged { + dec.LikeDistros = false + } + baseDir := filepath.Join(config.PkgsDir, vars.Name) srcdir := filepath.Join(baseDir, "src") pkgdir := filepath.Join(baseDir, "pkg") diff --git a/cli.go b/cli.go index e8515f3..fb91c5d 100644 --- a/cli.go +++ b/cli.go @@ -2,7 +2,6 @@ package main import ( "os" - "path/filepath" "github.com/AlecAivazis/survey/v2" "go.arsenm.dev/logger/log" @@ -50,16 +49,14 @@ func yesNoPrompt(msg string, def bool) (bool, error) { return answer, err } -func promptViewScript(script string) error { - name := filepath.Base(filepath.Dir(script)) - +func promptViewScript(script string, name string) error { view, err := yesNoPrompt("Would you like to view the build script for "+name, false) if err != nil { return err } if view { - err = showScript(script) + err = showScript(script, name) if err != nil { return err } @@ -77,7 +74,7 @@ func promptViewScript(script string) error { return nil } -func showScript(path string) error { +func showScript(path, name string) error { scriptFl, err := os.Open(path) if err != nil { return err @@ -89,6 +86,6 @@ func showScript(path string) error { return err } - pgr := pager.New(filepath.Base(filepath.Dir(path)), str) + pgr := pager.New(name, str) return pgr.Run() } diff --git a/helpers.go b/helpers.go index f0f69ae..1171020 100644 --- a/helpers.go +++ b/helpers.go @@ -35,6 +35,11 @@ var helpers = shutils.ExecFuncs{ "git-version": gitVersionCmd, } +// rHelpers contains restricted read-only helpers that don't modify any state +var rHelpers = shutils.ExecFuncs{ + "git-version": gitVersionCmd, +} + func installHelperCmd(prefix string, perms os.FileMode) shutils.ExecFunc { return func(hc interp.HandlerContext, cmd string, args []string) error { if len(args) < 1 { diff --git a/install.go b/install.go index 38a8355..176c608 100644 --- a/install.go +++ b/install.go @@ -101,11 +101,6 @@ func flattenFoundPkgs(found map[string][]db.Package, verb string) []db.Package { // installScripts builds and installs LURE build scripts func installScripts(ctx context.Context, mgr manager.Manager, scripts []string) { for _, script := range scripts { - err := promptViewScript(script) - if err != nil { - log.Fatal("Failed to prompt user to view build script").Err(err).Send() - } - builtPkgs, _, err := buildPackage(ctx, script, mgr) if err != nil { log.Fatal("Error building package").Err(err).Send() diff --git a/internal/shutils/exec.go b/internal/shutils/exec.go index 3a18fd4..667cdbd 100644 --- a/internal/shutils/exec.go +++ b/internal/shutils/exec.go @@ -39,18 +39,25 @@ type ExecFunc func(hc interp.HandlerContext, name string, args []string) error type ExecFuncs map[string]ExecFunc -func (ef ExecFuncs) ExecHandler(ctx context.Context, args []string) error { - name := args[0] +// ExecHandler returns a new ExecHandlerFunc that falls back to fallback +// if the command cannot be found in the map. If fallback is nil, the default +// handler is used. +func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc { + return func(ctx context.Context, args []string) error { + name := args[0] - if fn, ok := ef[name]; ok { - hctx := interp.HandlerCtx(ctx) - if len(args) > 1 { - return fn(hctx, args[0], args[1:]) - } else { - return fn(hctx, args[0], nil) + if fn, ok := ef[name]; ok { + hctx := interp.HandlerCtx(ctx) + if len(args) > 1 { + return fn(hctx, args[0], args[1:]) + } else { + return fn(hctx, args[0], nil) + } } - } - defExec := interp.DefaultExecHandler(2 * time.Second) - return defExec(ctx, args) + if fallback == nil { + fallback = interp.DefaultExecHandler(2 * time.Second) + } + return fallback(ctx, args) + } } diff --git a/internal/shutils/restricted.go b/internal/shutils/restricted.go new file mode 100644 index 0000000..b869f8a --- /dev/null +++ b/internal/shutils/restricted.go @@ -0,0 +1,76 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2022 Arsen Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package shutils + +import ( + "context" + "io" + "os" + "strings" + "time" + + "golang.org/x/exp/slices" + "mvdan.cc/sh/v3/interp" +) + +func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc { + return func(ctx context.Context, s string) ([]os.FileInfo, error) { + for _, allowedPrefix := range allowedPrefixes { + if strings.HasPrefix(s, allowedPrefix) { + return interp.DefaultReadDirHandler()(ctx, s) + } + } + + return nil, os.ErrNotExist + } +} + +func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc { + return func(ctx context.Context, s string, b bool) (os.FileInfo, error) { + for _, allowedPrefix := range allowedPrefixes { + if strings.HasPrefix(s, allowedPrefix) { + return interp.DefaultStatHandler()(ctx, s, b) + } + } + + return nil, os.ErrNotExist + } +} + +func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc { + return func(ctx context.Context, s string, i int, fm os.FileMode) (io.ReadWriteCloser, error) { + for _, allowedPrefix := range allowedPrefixes { + if strings.HasPrefix(s, allowedPrefix) { + return interp.DefaultOpenHandler()(ctx, s, i, fm) + } + } + + return NopRWC{}, nil + } +} + +func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc { + return func(ctx context.Context, args []string) error { + if slices.Contains(allowedCmds, args[0]) { + return interp.DefaultExecHandler(2*time.Second)(ctx, args) + } + + return nil + } +}