-
Notifications
You must be signed in to change notification settings - Fork 429
feat: Introduce shell metacharacter escaping for exec #491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c27dd75
9d08eac
8dcdf83
8c93cd8
9ab22e5
ff28fcc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,7 @@ import ( | |||||
| "github.com/stretchr/testify/require" | ||||||
|
|
||||||
| "github.com/NVIDIA/nvidia-container-toolkit/internal/modifier" | ||||||
| "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" | ||||||
| "github.com/NVIDIA/nvidia-container-toolkit/internal/test" | ||||||
| ) | ||||||
|
|
||||||
|
|
@@ -87,8 +88,7 @@ func TestBadInput(t *testing.T) { | |||||
| t.Fatal(err) | ||||||
| } | ||||||
|
|
||||||
| //nolint:gosec // TODO: Can we harden this so that there is less risk of command injection | ||||||
| cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle") | ||||||
| cmdCreate := exec.Command(oci.Escape([]string{nvidiaRuntime})[0], oci.Escape([]string{"create", "--bundle"})...) //nolint:gosec | ||||||
|
||||||
| cmdCreate := exec.Command(oci.Escape([]string{nvidiaRuntime})[0], oci.Escape([]string{"create", "--bundle"})...) //nolint:gosec | |
| cmdCreate := exec.Command(nvidiaRuntime, "create", "--bundle") //nolint:gosec |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, breaking the command. Remove the oci.Escape calls.
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, breaking the command. Remove the oci.Escape calls.
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, breaking the command. Remove the oci.Escape calls.
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escaping arguments passed to exec.Command is incorrect. The exec.Command function executes binaries directly without shell interpretation. The escaped strings will include literal backslashes, which will be interpreted as part of the file paths, causing the cp command to fail. Remove the oci.Escape call.
| cmd := exec.Command("cp", oci.Escape([]string{c.unmodifiedSpecFile(), c.specFilePath()})...) | |
| cmd := exec.Command("cp", c.unmodifiedSpecFile(), c.specFilePath()) |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -24,6 +24,7 @@ import ( | |||
| "github.com/sirupsen/logrus" | ||||
|
|
||||
| "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk-installer/container/operator" | ||||
| "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" | ||||
| "github.com/NVIDIA/nvidia-container-toolkit/pkg/config/engine" | ||||
| ) | ||||
|
|
||||
|
|
@@ -147,8 +148,8 @@ func (o Options) SystemdRestart(service string) error { | |||
|
|
||||
| logrus.Infof("Restarting %v%v using systemd: %v", service, msg, args) | ||||
|
|
||||
| //nolint:gosec // TODO: Can we harden this so that there is less risk of command injection | ||||
| cmd := exec.Command(args[0], args[1:]...) | ||||
| args = oci.Escape(args) | ||||
|
||||
| args = oci.Escape(args) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,20 +25,20 @@ import ( | |
| "syscall" | ||
|
|
||
| "github.com/opencontainers/runc/libcontainer/exeseal" | ||
|
|
||
| "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" | ||
| ) | ||
|
|
||
| // SafeExec attempts to clone the specified binary (as an memfd, for example) before executing it. | ||
| func SafeExec(path string, args []string, envv []string) error { | ||
| safeExe, err := cloneBinary(path) | ||
| if err != nil { | ||
| //nolint:gosec // TODO: Can we harden this so that there is less risk of command injection | ||
| return syscall.Exec(path, args, envv) | ||
| return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec | ||
|
||
| } | ||
| defer safeExe.Close() | ||
|
|
||
| exePath := "/proc/self/fd/" + strconv.Itoa(int(safeExe.Fd())) | ||
| //nolint:gosec // TODO: Can we harden this so that there is less risk of command injection | ||
| return syscall.Exec(exePath, args, envv) | ||
| return syscall.Exec(exePath, oci.Escape(args), envv) //nolint:gosec | ||
|
||
| } | ||
|
|
||
| func cloneBinary(path string) (*os.File, error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,11 +18,14 @@ | |||||
|
|
||||||
| package ldconfig | ||||||
|
|
||||||
| import "syscall" | ||||||
| import ( | ||||||
| "syscall" | ||||||
|
|
||||||
| "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" | ||||||
| ) | ||||||
|
|
||||||
| // SafeExec is not implemented on non-linux systems and forwards directly to the | ||||||
| // Exec syscall. | ||||||
| func SafeExec(path string, args []string, envv []string) error { | ||||||
| //nolint:gosec // TODO: Can we harden this so that there is less risk of command injection | ||||||
| return syscall.Exec(path, args, envv) | ||||||
| return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec | ||||||
|
||||||
| return syscall.Exec(path, oci.Escape(args), envv) //nolint:gosec | |
| return syscall.Exec(path, args, envv) //nolint:gosec |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,16 +19,28 @@ package oci | |||||
| import ( | ||||||
| "fmt" | ||||||
| "os" | ||||||
| "regexp" | ||||||
| "strings" | ||||||
| "syscall" | ||||||
| ) | ||||||
|
|
||||||
| // shellMetachars represents a set of shell metacharacters that are commonly | ||||||
| // used for shell scripting and may lead to security vulnerabilities if not | ||||||
| // properly handled. | ||||||
| // | ||||||
| // These metacharacters include: | & ; ( ) < > \t \n $ \ ` | ||||||
| const shellMetachars = "|&;()<> \t\n$\\`'\"" | ||||||
|
|
||||||
| // metacharRegex matches any shell metacharcter. | ||||||
|
||||||
| // metacharRegex matches any shell metacharcter. | |
| // metacharRegex matches any shell metacharacter. |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escaping arguments passed to syscall.Exec is incorrect. The syscall.Exec function directly invokes the execve system call, which does not interpret shell metacharacters - it passes arguments as-is to the new process. Adding backslash escaping will cause the literal backslashes to be passed to the program, breaking argument parsing. Shell escaping is only needed when passing commands through a shell interpreter (e.g., /bin/sh -c). Remove the Escape call here.
| args = Escape(args) | |
| // Do not escape arguments; syscall.Exec passes them as-is to the new process. |
servusdei2018 marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /* | ||
| # Copyright (c) 2025, NVIDIA CORPORATION. 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 oci | ||
|
|
||
| import ( | ||
| "reflect" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestEscape(t *testing.T) { | ||
| testCases := []struct { | ||
| name string | ||
| input []string | ||
| expected []string | ||
| }{ | ||
| { | ||
| name: "Empty Slice", | ||
| input: []string{}, | ||
| expected: []string{}, | ||
| }, | ||
| { | ||
| name: "Slice with no Metacharacters", | ||
| input: []string{"ls", "-l", "/home/user"}, | ||
| expected: []string{"ls", "-l", "/home/user"}, | ||
| }, | ||
| { | ||
| name: "Slice with some Metacharacters", | ||
| input: []string{"echo", "Hello World", "and", "goodbye | cat"}, | ||
| expected: []string{"echo", `Hello\ World`, `and`, `goodbye\ \|\ cat`}, | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range testCases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| actual := Escape(tc.input) | ||
| if !reflect.DeepEqual(actual, tc.expected) { | ||
| t.Errorf("Escape(%q) = %q; want %q", tc.input, actual, tc.expected) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escaping arguments passed to syscall.Exec is incorrect. The syscall.Exec function directly invokes the execve system call without shell interpretation, so shell metacharacter escaping will cause literal backslashes to be passed to the program. Remove the oci.Escape call.