fix: harden target recovery metadata safety
This commit is contained in:
@@ -4,12 +4,14 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"proxyctl/internal/gnome"
|
||||
"proxyctl/internal/operationlock"
|
||||
"proxyctl/internal/oplog"
|
||||
"proxyctl/internal/profile"
|
||||
@@ -206,6 +208,93 @@ func isolatedProcessEnvironment(root string) []string {
|
||||
)
|
||||
}
|
||||
|
||||
type passwordEchoRunner struct {
|
||||
values map[string]string
|
||||
failPasswordSet bool
|
||||
}
|
||||
|
||||
func newPasswordEchoRunner(savedPassword string) *passwordEchoRunner {
|
||||
return &passwordEchoRunner{values: map[string]string{
|
||||
"org.gnome.system.proxy mode": "'auto'", "org.gnome.system.proxy autoconfig-url": "''", "org.gnome.system.proxy ignore-hosts": "[]",
|
||||
"org.gnome.system.proxy.http host": "''", "org.gnome.system.proxy.http port": "0", "org.gnome.system.proxy.http use-authentication": "false",
|
||||
"org.gnome.system.proxy.http authentication-user": "''", "org.gnome.system.proxy.http authentication-password": "'" + savedPassword + "'",
|
||||
"org.gnome.system.proxy.https host": "''", "org.gnome.system.proxy.https port": "0", "org.gnome.system.proxy.socks host": "''", "org.gnome.system.proxy.socks port": "0",
|
||||
}}
|
||||
}
|
||||
|
||||
func (r *passwordEchoRunner) Run(_ context.Context, command gnome.Command) (string, error) {
|
||||
if len(command.Args) == 1 && command.Args[0] == "list-schemas" {
|
||||
return "org.gnome.system.proxy\norg.gnome.system.proxy.http\norg.gnome.system.proxy.https\norg.gnome.system.proxy.socks\n", nil
|
||||
}
|
||||
if len(command.Args) == 3 && command.Args[0] == "get" {
|
||||
return r.values[command.Args[1]+" "+command.Args[2]] + "\n", nil
|
||||
}
|
||||
if len(command.Args) == 4 && command.Args[0] == "set" {
|
||||
if r.failPasswordSet && command.Args[2] == "authentication-password" {
|
||||
return "", fmt.Errorf("runner echoed saved argument %s: %w", command.Args[3], os.ErrPermission)
|
||||
}
|
||||
r.values[command.Args[1]+" "+command.Args[2]] = command.Args[3]
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("unexpected command: %v", command.Args)
|
||||
}
|
||||
|
||||
func passwordRestoreFixture(t *testing.T, secret string) (*gnome.Adapter, *passwordEchoRunner) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
runner := newPasswordEchoRunner(secret)
|
||||
statePath := filepath.Join(root, "state", "gnome.json")
|
||||
adapter := gnome.New(gnome.Configuration{CommandName: "gsettings", UID: os.Getuid(), GID: os.Getgid(), HomeDir: root, StateFile: statePath}, runner, gnome.NewFileState(statePath, -1, -1, root))
|
||||
active := &profile.Profile{Name: "office", Mode: profile.AdvancedMode, HTTPURL: "http://proxy.example:8080", Bypass: []string{"localhost"}}
|
||||
if result := adapter.SetDesired(context.Background(), true, active); result.Err != nil || result.Inspection.State != target.Enabled {
|
||||
t.Fatalf("prepare managed GNOME Target: %#v", result)
|
||||
}
|
||||
runner.failPasswordSet = true
|
||||
return adapter, runner
|
||||
}
|
||||
|
||||
func TestResetSuppressesSavedGNOMEPasswordFromOperationCLIAndLogsWithoutActiveProfile(t *testing.T) {
|
||||
const secret = "GNOME-ORIGINAL-PASSWORD-FULL-CHAIN-07"
|
||||
|
||||
direct, _ := passwordRestoreFixture(t, secret)
|
||||
operation := direct.SetDesired(context.Background(), false, nil)
|
||||
if operation.Err == nil {
|
||||
t.Fatal("password restore unexpectedly succeeded")
|
||||
}
|
||||
if !errors.Is(operation.Err, os.ErrPermission) {
|
||||
t.Fatalf("OperationResult lost safe permission classification: %v", operation.Err)
|
||||
}
|
||||
if rendered := fmt.Sprint(operation); strings.Contains(rendered, secret) || strings.Contains(rendered, "runner echoed") {
|
||||
t.Fatalf("unsafe OperationResult: %s", rendered)
|
||||
}
|
||||
|
||||
managed, _ := passwordRestoreFixture(t, secret)
|
||||
root := t.TempDir()
|
||||
config := filepath.Join(root, "config", "profiles.json")
|
||||
writeConfig(t, config, `{"profiles":[]}`)
|
||||
store := profile.NewFileStore(config)
|
||||
stateDir := filepath.Join(root, "state")
|
||||
lock := operationlock.New(filepath.Join(stateDir, "operation.lock"))
|
||||
logger := oplog.New(filepath.Join(stateDir, "operations.log"), 1024, 1)
|
||||
var output bytes.Buffer
|
||||
err := runMaintenanceCommand([]string{"reset"}, strings.NewReader("yes\n"), &output, store, []target.Target{managed}, lock, logger, nil)
|
||||
if err == nil {
|
||||
t.Fatal("reset unexpectedly succeeded")
|
||||
}
|
||||
logData, readErr := os.ReadFile(filepath.Join(stateDir, "operations.log"))
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
for surface, value := range map[string]string{"CLI output": output.String(), "returned error": err.Error(), "operation log": string(logData)} {
|
||||
if strings.Contains(value, secret) || strings.Contains(value, "runner echoed") {
|
||||
t.Fatalf("%s exposed saved password or raw runner error: %q", surface, value)
|
||||
}
|
||||
if !strings.Contains(value, "sensitive argument and runner error suppressed") {
|
||||
t.Fatalf("%s lacks fixed safe failure message: %q", surface, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetCommandCancellationAndTruthfulPartialFailureWithFakeTargets(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
config := filepath.Join(root, "xdg-config", "proxyctl", "profiles.json")
|
||||
|
||||
Reference in New Issue
Block a user