1// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package rules
16
17import (
18	"go/ast"
19	"go/types"
20
21	"github.com/securego/gosec/v2"
22)
23
24type subprocess struct {
25	gosec.MetaData
26	gosec.CallList
27}
28
29func (r *subprocess) ID() string {
30	return r.MetaData.ID
31}
32
33// TODO(gm) The only real potential for command injection with a Go project
34// is something like this:
35//
36// syscall.Exec("/bin/sh", []string{"-c", tainted})
37//
38// E.g. Input is correctly escaped but the execution context being used
39// is unsafe. For example:
40//
41// syscall.Exec("echo", "foobar" + tainted)
42func (r *subprocess) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) {
43	if node := r.ContainsPkgCallExpr(n, c, false); node != nil {
44		args := node.Args
45		if r.isContext(n, c) {
46			args = args[1:]
47		}
48		for _, arg := range args {
49			if ident, ok := arg.(*ast.Ident); ok {
50				obj := c.Info.ObjectOf(ident)
51				if _, ok := obj.(*types.Var); ok && !gosec.TryResolve(ident, c) {
52					return gosec.NewIssue(c, n, r.ID(), "Subprocess launched with variable", gosec.Medium, gosec.High), nil
53				}
54			} else if !gosec.TryResolve(arg, c) {
55				// the arg is not a constant or a variable but instead a function call or os.Args[i]
56				return gosec.NewIssue(c, n, r.ID(), "Subprocess launched with function call as argument or cmd arguments", gosec.Medium, gosec.High), nil
57			}
58		}
59	}
60	return nil, nil
61}
62
63// isContext checks whether or not the node is a CommandContext call or not
64// Thi is requried in order to skip the first argument from the check.
65func (r *subprocess) isContext(n ast.Node, ctx *gosec.Context) bool {
66	selector, indent, err := gosec.GetCallInfo(n, ctx)
67	if err != nil {
68		return false
69	}
70	if selector == "exec" && indent == "CommandContext" {
71		return true
72	}
73	return false
74}
75
76// NewSubproc detects cases where we are forking out to an external process
77func NewSubproc(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
78	rule := &subprocess{gosec.MetaData{ID: id}, gosec.NewCallList()}
79	rule.Add("os/exec", "Command")
80	rule.Add("os/exec", "CommandContext")
81	rule.Add("syscall", "Exec")
82	rule.Add("syscall", "ForkExec")
83	rule.Add("syscall", "StartProcess")
84	return rule, []ast.Node{(*ast.CallExpr)(nil)}
85}
86