Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ reporters and reviewers have context:
- Shell-outs (`apt list --upgradable`, optional `vcgencmd measure_temp`) are
invoked with fixed argument lists (no user input is interpolated into
shell commands), to avoid command injection.
- Shell-outs are also invoked with an explicit, minimal environment rather
than inheriting the service's own β€” neither `apt` nor `vcgencmd` needs
anything beyond `PATH` (and, for `apt`, a couple of apt-specific
variables), so `PIMONITOR_API_KEY` is never copied into a child process's
`/proc/<pid>/environ`.

If you believe any of these assumptions are violated by the current
implementation, please report it as described above.
10 changes: 9 additions & 1 deletion internal/collector/vcgencmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,15 @@ func (r *vcgencmdRunner) run(ctx context.Context, subcommand string) (string, er
ctx, cancel := context.WithTimeout(ctx, vcgencmdTimeout)
defer cancel()

out, err := exec.CommandContext(ctx, path, subcommand).Output()
cmd := exec.CommandContext(ctx, path, subcommand)
// Build the child environment explicitly rather than inheriting the
// service's: PiMonitor's own environment may carry PIMONITOR_API_KEY, and
// there is no reason for that secret to be visible in vcgencmd's
// /proc/<pid>/environ. path is already the absolute location resolved by
// exec.LookPath at detection time, so no PATH is needed either.
cmd.Env = []string{}

out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("run vcgencmd %s: %w", subcommand, err)
}
Expand Down
32 changes: 32 additions & 0 deletions internal/collector/vcgencmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package collector
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -88,6 +92,34 @@ func TestVcgencmdRunner_Run_CommandFails(t *testing.T) {
}
}

func TestVcgencmdRunner_Run_DoesNotLeakParentEnvironment(t *testing.T) {
t.Setenv("PIMONITOR_API_KEY", "test-secret-should-not-leak")

dir := t.TempDir()
envFile := filepath.Join(dir, "env.out")
path := writeFakeVcgencmd(t, dir, "fake-vcgencmd", fmt.Sprintf("env > %q", envFile))
r := &vcgencmdRunner{detected: true, path: path}

if _, err := r.run(context.Background(), "measure_temp"); err != nil {
t.Fatalf("run: %v", err)
}

out, err := os.ReadFile(envFile)
if err != nil {
t.Fatalf("read captured child environment: %v", err)
}
if strings.Contains(string(out), "PIMONITOR_API_KEY") {
t.Fatalf("child environment leaked PIMONITOR_API_KEY: %q", out)
}
// PATH is not required: path is already the absolute location resolved
// by exec.LookPath at detection time, so its absence here confirms the
// environment was built explicitly rather than inherited (PWD is set by
// the shell itself, not inherited, so its presence is expected).
if strings.Contains(string(out), "PATH=") {
t.Fatalf("child environment unexpectedly contains PATH: %q", out)
}
}

func TestNewVcgencmdRunner_NilClockDefaultsToTimeNow(t *testing.T) {
r := newVcgencmdRunner(nil)

Expand Down
Loading