Skip to content

Commit c27dd75

Browse files
committed
feat: introduce shell metacharacter escaping for exec
Signed-off-by: Nate Bracy <nate@bracy.dev>
1 parent 1a58392 commit c27dd75

File tree

8 files changed

+117
-29
lines changed

8 files changed

+117
-29
lines changed

cmd/nvidia-container-runtime-hook/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/NVIDIA/nvidia-container-toolkit/internal/info"
1818
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
1919
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
20+
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
2021
)
2122

2223
var (
@@ -150,8 +151,8 @@ func doPrestart() {
150151
args = append(args, rootfs)
151152

152153
env := append(os.Environ(), cli.Environment...)
153-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection?
154-
err = syscall.Exec(args[0], args, env)
154+
args = oci.Escape(args)
155+
err = syscall.Exec(args[0], args, env) //nolint:gosec
155156
log.Panicln("exec failed:", err)
156157
}
157158

cmd/nvidia-container-runtime/main_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/stretchr/testify/require"
1717

1818
"github.com/NVIDIA/nvidia-container-toolkit/internal/modifier"
19+
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
1920
"github.com/NVIDIA/nvidia-container-toolkit/internal/test"
2021
)
2122

@@ -87,8 +88,7 @@ func TestBadInput(t *testing.T) {
8788
t.Fatal(err)
8889
}
8990

90-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
91-
cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle")
91+
cmdCreate := exec.Command(oci.Escape1(nvidiaRuntime), oci.Escape([]string{"create", "--bundle"})...) //nolint:gosec
9292
t.Logf("executing: %s\n", strings.Join(cmdCreate.Args, " "))
9393
err = cmdCreate.Run()
9494
require.Error(t, err, "runtime should return an error")
@@ -105,8 +105,8 @@ func TestGoodInput(t *testing.T) {
105105
t.Fatalf("error generating runtime spec: %v", err)
106106
}
107107

108-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
109-
cmdRun := exec.Command(nvidiaRuntime, "run", "--bundle", cfg.bundlePath(), "testcontainer")
108+
//nolint:gosec
109+
cmdRun := exec.Command(oci.Escape1(nvidiaRuntime), oci.Escape([]string{"run", "--bundle", cfg.bundlePath(), "testcontainer"})...)
110110
t.Logf("executing: %s\n", strings.Join(cmdRun.Args, " "))
111111
output, err := cmdRun.CombinedOutput()
112112
require.NoErrorf(t, err, "runtime should not return an error", "output=%v", string(output))
@@ -116,8 +116,8 @@ func TestGoodInput(t *testing.T) {
116116
require.NoError(t, err, "should be no errors when reading and parsing spec from config.json")
117117
require.Empty(t, spec.Hooks, "there should be no hooks in config.json")
118118

119-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
120-
cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle", cfg.bundlePath(), "testcontainer")
119+
//nolint:gosec
120+
cmdCreate := exec.Command(oci.Escape1(nvidiaRuntime), oci.Escape([]string{"create", "--bundle", cfg.bundlePath(), "testcontainer"})...)
121121
t.Logf("executing: %s\n", strings.Join(cmdCreate.Args, " "))
122122
err = cmdCreate.Run()
123123
require.NoError(t, err, "runtime should not return an error")
@@ -161,8 +161,8 @@ func TestDuplicateHook(t *testing.T) {
161161
}
162162

163163
// Test how runtime handles already existing prestart hook in config.json
164-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
165-
cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle", cfg.bundlePath(), "testcontainer")
164+
//nolint:gosec
165+
cmdCreate := exec.Command(oci.Escape1(nvidiaRuntime), oci.Escape([]string{"create", "--bundle", cfg.bundlePath(), "testcontainer"})...)
166166
t.Logf("executing: %s\n", strings.Join(cmdCreate.Args, " "))
167167
output, err := cmdCreate.CombinedOutput()
168168
require.NoErrorf(t, err, "runtime should not return an error", "output=%v", string(output))
@@ -230,8 +230,8 @@ func (c testConfig) generateNewRuntimeSpec() error {
230230
return err
231231
}
232232

233-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
234-
cmd := exec.Command("cp", c.unmodifiedSpecFile(), c.specFilePath())
233+
//nolint:gosec
234+
cmd := exec.Command(oci.Escape1("cp"), oci.Escape([]string{c.unmodifiedSpecFile(), c.specFilePath()})...)
235235
err = cmd.Run()
236236
if err != nil {
237237
return err

cmd/nvidia-ctk-installer/container/container.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/sirupsen/logrus"
2525

2626
"github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk-installer/container/operator"
27+
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
2728
"github.com/NVIDIA/nvidia-container-toolkit/pkg/config/engine"
2829
)
2930

@@ -147,8 +148,8 @@ func (o Options) SystemdRestart(service string) error {
147148

148149
logrus.Infof("Restarting %v%v using systemd: %v", service, msg, args)
149150

150-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
151-
cmd := exec.Command(args[0], args[1:]...)
151+
args = oci.Escape(args)
152+
cmd := exec.Command(args[0], args[1:]...) //nolint:gosec
152153
cmd.Stdout = os.Stdout
153154
cmd.Stderr = os.Stderr
154155
err := cmd.Run()

internal/ldconfig/safe-exec_linux.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ import (
2525
"syscall"
2626

2727
"github.com/opencontainers/runc/libcontainer/exeseal"
28+
29+
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
2830
)
2931

3032
// SafeExec attempts to clone the specified binary (as an memfd, for example) before executing it.
3133
func SafeExec(path string, args []string, envv []string) error {
3234
safeExe, err := cloneBinary(path)
3335
if err != nil {
34-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
35-
return syscall.Exec(path, args, envv)
36+
return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec
3637
}
3738
defer safeExe.Close()
3839

3940
exePath := "/proc/self/fd/" + strconv.Itoa(int(safeExe.Fd()))
40-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
41-
return syscall.Exec(exePath, args, envv)
41+
return syscall.Exec(exePath, oci.Escape(args), envv) //nolint:gosec
4242
}
4343

4444
func cloneBinary(path string) (*os.File, error) {

internal/ldconfig/safe-exec_other.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818

1919
package ldconfig
2020

21-
import "syscall"
21+
import (
22+
"syscall"
23+
24+
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
25+
)
2226

2327
// SafeExec is not implemented on non-linux systems and forwards directly to the
2428
// Exec syscall.
2529
func SafeExec(path string, args []string, envv []string) error {
26-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
27-
return syscall.Exec(path, args, envv)
30+
return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec
2831
}

internal/nvsandboxutils/zz_generated.api.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ package nvsandboxutils
2020

2121
// The variables below represent package level methods from the library type.
2222
var (
23-
ErrorString = libnvsandboxutils.ErrorString
23+
ErrorString = libnvsandboxutils.ErrorString
2424
GetDriverVersion = libnvsandboxutils.GetDriverVersion
25-
GetFileContent = libnvsandboxutils.GetFileContent
26-
GetGpuResource = libnvsandboxutils.GetGpuResource
27-
Init = libnvsandboxutils.Init
28-
LookupSymbol = libnvsandboxutils.LookupSymbol
29-
Shutdown = libnvsandboxutils.Shutdown
25+
GetFileContent = libnvsandboxutils.GetFileContent
26+
GetGpuResource = libnvsandboxutils.GetGpuResource
27+
Init = libnvsandboxutils.Init
28+
LookupSymbol = libnvsandboxutils.LookupSymbol
29+
Shutdown = libnvsandboxutils.Shutdown
3030
)
3131

3232
// Interface represents the interface for the library type.

internal/oci/runtime_syscall_exec.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,25 @@ package oci
1919
import (
2020
"fmt"
2121
"os"
22+
"regexp"
23+
"strings"
2224
"syscall"
2325
)
2426

27+
// shellMetachars represents a set of shell metacharacters that are commonly
28+
// used for shell scripting and may lead to security vulnerabilities if not
29+
// properly handled.
30+
//
31+
// These metacharacters include: | & ; ( ) < > \t \n $ \ `
32+
const shellMetachars = "|&;()<> \t\n$\\`"
33+
2534
type syscallExec struct{}
2635

2736
var _ Runtime = (*syscallExec)(nil)
2837

2938
func (r syscallExec) Exec(args []string) error {
30-
//nolint:gosec // TODO: Can we harden this so that there is less risk of command injection
31-
err := syscall.Exec(args[0], args, os.Environ())
39+
args = Escape(args)
40+
err := syscall.Exec(args[0], args, os.Environ()) //nolint:gosec
3241
if err != nil {
3342
return fmt.Errorf("could not exec '%v': %v", args[0], err)
3443
}
@@ -41,3 +50,22 @@ func (r syscallExec) Exec(args []string) error {
4150
func (r syscallExec) String() string {
4251
return "exec"
4352
}
53+
54+
// Escape1 escapes shell metacharacters in a single command-line argument.
55+
func Escape1(arg string) string {
56+
if strings.ContainsAny(arg, shellMetachars) {
57+
e := regexp.MustCompile(`([|&;()<> \t\n$\\`+"`"+`])`).ReplaceAllString(arg, `\$1`)
58+
return fmt.Sprintf(`"%s"`, e)
59+
}
60+
return arg
61+
}
62+
63+
// Escape escapes shell metacharacters in a slice of command-line arguments
64+
// and returns a new slice containing the escaped arguments.
65+
func Escape(args []string) []string {
66+
escaped := make([]string, len(args))
67+
for i := range args {
68+
escaped[i] = Escape1(args[i])
69+
}
70+
return escaped
71+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
*/
16+
17+
package oci
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
)
23+
24+
func TestEscape(t *testing.T) {
25+
testCases := []struct {
26+
name string
27+
input []string
28+
expected []string
29+
}{
30+
{
31+
name: "Empty Slice",
32+
input: []string{},
33+
expected: []string{},
34+
},
35+
{
36+
name: "Slice with no Metacharacters",
37+
input: []string{"ls", "-l", "/home/user"},
38+
expected: []string{"ls", "-l", "/home/user"},
39+
},
40+
{
41+
name: "Slice with some Metacharacters",
42+
input: []string{"echo", "Hello World", "and", "goodbye | cat"},
43+
expected: []string{"echo", `"Hello\ World"`, "and", `"goodbye\ \|\ cat"`},
44+
},
45+
}
46+
47+
for _, tc := range testCases {
48+
t.Run(tc.name, func(t *testing.T) {
49+
actual := Escape(tc.input)
50+
if !reflect.DeepEqual(actual, tc.expected) {
51+
t.Errorf("Escape(%q) = %q; want %q", tc.input, actual, tc.expected)
52+
}
53+
})
54+
}
55+
}

0 commit comments

Comments
 (0)