diff --git a/common/hexec/safeCommand.go b/common/hexec/safeCommand.go new file mode 100644 index 00000000000..6d5c7398235 --- /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 13e66820198..9f65a1b4a2b 100644 --- a/create/content.go +++ b/create/content.go @@ -21,10 +21,10 @@ import ( "io" "os" - "os/exec" "path/filepath" "strings" + "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs" @@ -106,7 +106,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 ee27120bcd8..3410dbaac20 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 6225d9a2483..0c9ac732322 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +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/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= diff --git a/hugolib/js_test.go b/hugolib/js_test.go index b4f1d21577c..05d9454b917 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{}) @@ -194,7 +197,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 e6e4ed8d3c6..cd1751c21a8 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 7573199aacd..67ce227c442 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -23,7 +23,8 @@ import ( "math/rand" "os" - "os/exec" + "github.com/gohugoio/hugo/common/hexec" + "path/filepath" "runtime" "strings" @@ -952,7 +953,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 a92b6f9e3ce..33c73bee607 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" @@ -194,7 +195,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 fc7fddb2353..d0ad8b411b7 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" ) @@ -14,12 +16,16 @@ func ExternallyRenderContent( 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") { @@ -41,9 +47,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 074e97d96ec..74c71c7be96 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" @@ -66,7 +65,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 cbc15c81a47..89d60c1347d 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 7d2175c94f3..da3a4fff438 100644 --- a/modules/client.go +++ b/modules/client.go @@ -26,6 +26,8 @@ import ( "path/filepath" "regexp" + "github.com/gohugoio/hugo/common/hexec" + hglob "github.com/gohugoio/hugo/hugofs/glob" "github.com/gobwas/glob" @@ -545,7 +547,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 19cf072eed6..0ae9d1cc807 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 61b9d211ffe..13748d7925b 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 a9d39d31b16..4c822790e86 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" @@ -107,10 +108,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 daeb7212a40..4743be3d588 100644 --- a/resources/resource_transformers/postcss/postcss.go +++ b/resources/resource_transformers/postcss/postcss.go @@ -25,6 +25,10 @@ import ( "strconv" "strings" + "github.com/cli/safeexec" + + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/loggers" @@ -36,8 +40,6 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/pkg/errors" - "os/exec" - "github.com/mitchellh/mapstructure" "github.com/gohugoio/hugo/common/herrors" @@ -148,10 +150,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 } @@ -189,7 +191,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 04202b25444..f995f1c3c7b 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" @@ -200,7 +201,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)) @@ -208,7 +209,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)) @@ -216,7 +217,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 90044570d37..f5ea398fb88 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 {