diff --git a/common/hexec/safeCommand.go b/common/hexec/safeCommand.go new file mode 100644 index 000000000..6d5c73982 --- /dev/null +++ b/common/hexec/safeCommand.go @@ -0,0 +1,45 @@ +// Copyright 2020 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hexec + +import ( + "context" + + "os/exec" + + "github.com/cli/safeexec" +) + +// SafeCommand is a wrapper around os/exec Command which uses a LookPath +// implementation that does not search in current directory before looking in PATH. +// See https://github.com/cli/safeexec and the linked issues. +func SafeCommand(name string, arg ...string) (*exec.Cmd, error) { + bin, err := safeexec.LookPath(name) + if err != nil { + return nil, err + } + + return exec.Command(bin, arg...), nil +} + +// SafeCommandContext wraps CommandContext +// See SafeCommand for more context. +func SafeCommandContext(ctx context.Context, name string, arg ...string) (*exec.Cmd, error) { + bin, err := safeexec.LookPath(name) + if err != nil { + return nil, err + } + + return exec.CommandContext(ctx, bin, arg...), nil +} diff --git a/create/content.go b/create/content.go index d1594a199..26eda2031 100644 --- a/create/content.go +++ b/create/content.go @@ -18,12 +18,12 @@ import ( "bytes" "io" "os" - "os/exec" "path/filepath" "strings" "github.com/pkg/errors" + "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs" @@ -105,7 +105,10 @@ func NewContent( jww.FEEDBACK.Printf("Editing %s with %q ...\n", targetPath, editor) editorCmd := append(strings.Fields(editor), contentPath) - cmd := exec.Command(editorCmd[0], editorCmd[1:]...) + cmd, err := hexec.SafeCommand(editorCmd[0], editorCmd[1:]...) + if err != nil { + return err + } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/go.mod b/go.mod index bcf7b3200..d3a5b4af4 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/bep/gitmap v1.1.2 github.com/bep/golibsass v0.7.0 github.com/bep/tmc v0.5.1 + github.com/cli/safeexec v1.0.0 github.com/disintegration/gift v1.2.1 github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dustin/go-humanize v1.0.0 diff --git a/go.sum b/go.sum index cd66e5956..e46d649d9 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgk github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= diff --git a/hugolib/js_test.go b/hugolib/js_test.go index fbbd335b3..8d8e015a6 100644 --- a/hugolib/js_test.go +++ b/hugolib/js_test.go @@ -16,11 +16,12 @@ package hugolib import ( "fmt" "os" - "os/exec" "path/filepath" "runtime" "testing" + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/htesting" "github.com/spf13/viper" @@ -125,7 +126,9 @@ TS: {{ template "print" $ts }} b.WithSourceFile("assets/js/included.js", includedJS) - out, err := exec.Command("npm", "install").CombinedOutput() + cmd, err := hexec.SafeCommand("npm", "install") + b.Assert(err, qt.IsNil) + out, err := cmd.CombinedOutput() b.Assert(err, qt.IsNil, qt.Commentf(string(out))) b.Build(BuildCfg{}) @@ -193,7 +196,8 @@ require github.com/gohugoio/hugoTestProjectJSModImports v0.5.0 // indirect }`) b.Assert(os.Chdir(workDir), qt.IsNil) - _, err = exec.Command("npm", "install").CombinedOutput() + cmd, _ := hexec.SafeCommand("npm", "install") + _, err = cmd.CombinedOutput() b.Assert(err, qt.IsNil) b.Build(BuildCfg{}) diff --git a/hugolib/resource_chain_babel_test.go b/hugolib/resource_chain_babel_test.go index e56c037f1..da03c83c7 100644 --- a/hugolib/resource_chain_babel_test.go +++ b/hugolib/resource_chain_babel_test.go @@ -16,11 +16,12 @@ package hugolib import ( "bytes" "os" - "os/exec" "path/filepath" "runtime" "testing" + "github.com/gohugoio/hugo/common/hexec" + jww "github.com/spf13/jwalterweatherman" "github.com/gohugoio/hugo/htesting" @@ -111,7 +112,8 @@ Transpiled: {{ $transpiled.Content | safeJS }} b.WithSourceFile("babel.config.js", babelConfig) b.Assert(os.Chdir(workDir), qt.IsNil) - _, err = exec.Command("npm", "install").CombinedOutput() + cmd, _ := hexec.SafeCommand("npm", "install") + _, err = cmd.CombinedOutput() b.Assert(err, qt.IsNil) b.Build(BuildCfg{}) diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 4f93115f9..b5baa4d0b 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -19,13 +19,15 @@ import ( "io" "math/rand" "os" - "os/exec" + "path/filepath" "runtime" "strings" "testing" "time" + "github.com/gohugoio/hugo/common/hexec" + jww "github.com/spf13/jwalterweatherman" "github.com/gohugoio/hugo/common/herrors" @@ -930,7 +932,8 @@ class-in-b { b.WithSourceFile("postcss.config.js", postcssConfig) b.Assert(os.Chdir(workDir), qt.IsNil) - _, err = exec.Command("npm", "install").CombinedOutput() + cmd, err := hexec.SafeCommand("npm", "install") + _, err = cmd.CombinedOutput() b.Assert(err, qt.IsNil) b.Build(BuildCfg{}) diff --git a/markup/asciidocext/convert.go b/markup/asciidocext/convert.go index e2e5b7865..a5465fe9f 100644 --- a/markup/asciidocext/convert.go +++ b/markup/asciidocext/convert.go @@ -18,9 +18,10 @@ package asciidocext import ( "bytes" - "os/exec" "path/filepath" + "github.com/cli/safeexec" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config" "github.com/gohugoio/hugo/markup/converter" @@ -193,7 +194,7 @@ func (a *asciidocConverter) appendArg(args []string, option, value, defaultValue } func getAsciidoctorExecPath() string { - path, err := exec.LookPath("asciidoctor") + path, err := safeexec.LookPath("asciidoctor") if err != nil { return "" } diff --git a/markup/internal/external.go b/markup/internal/external.go index e8f86ae94..0937afa34 100644 --- a/markup/internal/external.go +++ b/markup/internal/external.go @@ -2,9 +2,11 @@ package internal import ( "bytes" - "os/exec" "strings" + "github.com/cli/safeexec" + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/markup/converter" ) @@ -13,12 +15,16 @@ func ExternallyRenderContent( ctx converter.DocumentContext, content []byte, path string, args []string) []byte { logger := cfg.Logger - cmd := exec.Command(path, args...) + cmd, err := hexec.SafeCommand(path, args...) + if err != nil { + logger.Errorf("%s rendering %s: %v", path, ctx.DocumentName, err) + return nil + } cmd.Stdin = bytes.NewReader(content) var out, cmderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &cmderr - err := cmd.Run() + err = cmd.Run() // Most external helpers exit w/ non-zero exit code only if severe, i.e. // halting errors occurred. -> log stderr output regardless of state of err for _, item := range strings.Split(cmderr.String(), "\n") { @@ -40,9 +46,9 @@ func normalizeExternalHelperLineFeeds(content []byte) []byte { } func GetPythonExecPath() string { - path, err := exec.LookPath("python") + path, err := safeexec.LookPath("python") if err != nil { - path, err = exec.LookPath("python.exe") + path, err = safeexec.LookPath("python.exe") if err != nil { return "" } diff --git a/markup/pandoc/convert.go b/markup/pandoc/convert.go index bbf619ce6..63bab2748 100644 --- a/markup/pandoc/convert.go +++ b/markup/pandoc/convert.go @@ -15,8 +15,7 @@ package pandoc import ( - "os/exec" - + "github.com/cli/safeexec" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/internal" @@ -65,7 +64,7 @@ func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentCon } func getPandocExecPath() string { - path, err := exec.LookPath("pandoc") + path, err := safeexec.LookPath("pandoc") if err != nil { return "" } diff --git a/markup/rst/convert.go b/markup/rst/convert.go index c397bf5fe..faed56276 100644 --- a/markup/rst/convert.go +++ b/markup/rst/convert.go @@ -16,9 +16,10 @@ package rst import ( "bytes" - "os/exec" "runtime" + "github.com/cli/safeexec" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/internal" @@ -96,9 +97,9 @@ func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext) } func getRstExecPath() string { - path, err := exec.LookPath("rst2html") + path, err := safeexec.LookPath("rst2html") if err != nil { - path, err = exec.LookPath("rst2html.py") + path, err = safeexec.LookPath("rst2html.py") if err != nil { return "" } diff --git a/modules/client.go b/modules/client.go index 88c1e933e..da14d58f4 100644 --- a/modules/client.go +++ b/modules/client.go @@ -28,6 +28,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/common/hexec" + hglob "github.com/gohugoio/hugo/hugofs/glob" "github.com/gobwas/glob" @@ -537,7 +539,10 @@ func (c *Client) runGo( } stderr := new(bytes.Buffer) - cmd := exec.CommandContext(ctx, "go", args...) + cmd, err := hexec.SafeCommandContext(ctx, "go", args...) + if err != nil { + return err + } cmd.Env = c.environ cmd.Dir = c.ccfg.WorkingDir diff --git a/releaser/git.go b/releaser/git.go index 7d2d43e2a..4db1c2329 100644 --- a/releaser/git.go +++ b/releaser/git.go @@ -15,11 +15,12 @@ package releaser import ( "fmt" - "os/exec" "regexp" "sort" "strconv" "strings" + + "github.com/gohugoio/hugo/common/hexec" ) var issueRe = regexp.MustCompile(`(?i)[Updates?|Closes?|Fix.*|See] #(\d+)`) @@ -148,7 +149,7 @@ func extractIssues(body string) []int { type gitInfos []gitInfo func git(args ...string) (string, error) { - cmd := exec.Command("git", args...) + cmd, _ := hexec.SafeCommand("git", args...) out, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args) diff --git a/releaser/releaser.go b/releaser/releaser.go index 97ffe1a2c..5c5a34787 100644 --- a/releaser/releaser.go +++ b/releaser/releaser.go @@ -20,11 +20,12 @@ import ( "io/ioutil" "log" "os" - "os/exec" "path/filepath" "regexp" "strings" + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/hugo" "github.com/pkg/errors" ) @@ -266,7 +267,7 @@ func (r *ReleaseHandler) release(releaseNotesFile string) error { args = append(args, "--skip-publish") } - cmd := exec.Command("goreleaser", args...) + cmd, _ := hexec.SafeCommand("goreleaser", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() diff --git a/resources/resource_transformers/babel/babel.go b/resources/resource_transformers/babel/babel.go index 64d8667bd..204153705 100644 --- a/resources/resource_transformers/babel/babel.go +++ b/resources/resource_transformers/babel/babel.go @@ -16,10 +16,11 @@ package babel import ( "bytes" "io" - "os/exec" "path/filepath" "strconv" + "github.com/cli/safeexec" + "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/hugo" @@ -108,10 +109,10 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx binary := csiBinPath - if _, err := exec.LookPath(binary); err != nil { + if _, err := safeexec.LookPath(binary); err != nil { // Try PATH binary = binaryName - if _, err := exec.LookPath(binary); err != nil { + if _, err := safeexec.LookPath(binary); err != nil { // This may be on a CI server etc. Will fall back to pre-built assets. return herrors.ErrFeatureNotAvailable } @@ -152,7 +153,10 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx } cmdArgs = append(cmdArgs, "--filename="+ctx.SourcePath) - cmd := exec.Command(binary, cmdArgs...) + cmd, err := hexec.SafeCommand(binary, cmdArgs...) + if err != nil { + return err + } cmd.Stdout = ctx.To cmd.Stderr = io.MultiWriter(infoW, &errBuf) diff --git a/resources/resource_transformers/postcss/postcss.go b/resources/resource_transformers/postcss/postcss.go index 27864b0c5..652770078 100644 --- a/resources/resource_transformers/postcss/postcss.go +++ b/resources/resource_transformers/postcss/postcss.go @@ -19,13 +19,16 @@ import ( "encoding/hex" "io" "io/ioutil" - "os/exec" "path" "path/filepath" "regexp" "strconv" "strings" + "github.com/cli/safeexec" + + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/loggers" @@ -146,10 +149,10 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC binary := csiBinPath - if _, err := exec.LookPath(binary); err != nil { + if _, err := safeexec.LookPath(binary); err != nil { // Try PATH binary = binaryName - if _, err := exec.LookPath(binary); err != nil { + if _, err := safeexec.LookPath(binary); err != nil { // This may be on a CI server etc. Will fall back to pre-built assets. return herrors.ErrFeatureNotAvailable } @@ -186,7 +189,10 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC cmdArgs = append(cmdArgs, optArgs...) } - cmd := exec.Command(binary, cmdArgs...) + cmd, err := hexec.SafeCommand(binary, cmdArgs...) + if err != nil { + return err + } var errBuf bytes.Buffer infoW := loggers.LoggerToWriterWithPrefix(logger.Info(), "postcss") diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go index c295ab734..b66c8a111 100644 --- a/scripts/fork_go_templates/main.go +++ b/scripts/fork_go_templates/main.go @@ -5,11 +5,12 @@ import ( "io/ioutil" "log" "os" - "os/exec" "path/filepath" "regexp" "strings" + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/hugio" "github.com/spf13/afero" @@ -203,7 +204,7 @@ func removeAll(expression, content string) string { } func rewrite(filename, rule string) { - cmf := exec.Command("gofmt", "-w", "-r", rule, filename) + cmf, _ := hexec.SafeCommand("gofmt", "-w", "-r", rule, filename) out, err := cmf.CombinedOutput() if err != nil { log.Fatal("gofmt failed:", string(out)) @@ -211,7 +212,7 @@ func rewrite(filename, rule string) { } func goimports(dir string) { - cmf := exec.Command("goimports", "-w", dir) + cmf, _ := hexec.SafeCommand("goimports", "-w", dir) out, err := cmf.CombinedOutput() if err != nil { log.Fatal("goimports failed:", string(out)) @@ -219,7 +220,7 @@ func goimports(dir string) { } func gofmt(dir string) { - cmf := exec.Command("gofmt", "-w", dir) + cmf, _ := hexec.SafeCommand("gofmt", "-w", dir) out, err := cmf.CombinedOutput() if err != nil { log.Fatal("gofmt failed:", string(out)) diff --git a/tpl/internal/go_templates/testenv/testenv.go b/tpl/internal/go_templates/testenv/testenv.go index 90044570d..f5ea398fb 100644 --- a/tpl/internal/go_templates/testenv/testenv.go +++ b/tpl/internal/go_templates/testenv/testenv.go @@ -13,7 +13,6 @@ package testenv import ( "errors" "flag" - "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" "os" "os/exec" "path/filepath" @@ -22,6 +21,9 @@ import ( "strings" "sync" "testing" + + "github.com/cli/safeexec" + "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg" ) // Builder reports the name of the builder running this test @@ -111,7 +113,7 @@ func GoTool() (string, error) { if _, err := os.Stat(path); err == nil { return path, nil } - goBin, err := exec.LookPath("go" + exeSuffix) + goBin, err := safeexec.LookPath("go" + exeSuffix) if err != nil { return "", errors.New("cannot find go tool: " + err.Error()) } @@ -162,7 +164,7 @@ func MustHaveExecPath(t testing.TB, path string) { err, found := execPaths.Load(path) if !found { - _, err = exec.LookPath(path) + _, err = safeexec.LookPath(path) err, _ = execPaths.LoadOrStore(path, err) } if err != nil {