fix: harden target recovery metadata safety

This commit is contained in:
proxyctl coordinator
2026-07-17 10:59:14 +08:00
parent a5360028ca
commit 613fe56540
7 changed files with 436 additions and 10 deletions
+51 -3
View File
@@ -22,8 +22,9 @@ import (
)
const (
integrationStart = "# >>> proxyctl shell integration >>>"
integrationEnd = "# <<< proxyctl shell integration <<<"
integrationStart = "# >>> proxyctl shell integration >>>"
integrationEnd = "# <<< proxyctl shell integration <<<"
shellRecoveryVersion = 1
)
var proxyVariables = []string{
@@ -149,7 +150,8 @@ func (f OSFileSystem) AtomicWrite(path string, data []byte, mode os.FileMode, ow
}
type recovery struct {
Original map[string]string `json:"original,omitempty"`
Version int `json:"version"`
Original map[string]string `json:"original"`
OriginalScript *string `json:"originalScript,omitempty"`
Fingerprint string `json:"fingerprint"`
}
@@ -288,6 +290,7 @@ func (a *Adapter) enable(active profile.Profile) error {
return fmt.Errorf("read Shell recovery metadata: %w", err)
}
if !managed {
metadata.Version = shellRecoveryVersion
if existing, readErr := a.files.ReadFile(a.configuration.EnvironmentFile); readErr == nil && recognizesProxy(existing) {
original := string(existing)
metadata.OriginalScript = &original
@@ -344,12 +347,57 @@ func (a *Adapter) readRecovery() (recovery, bool, error) {
if err != nil {
return recovery{}, false, err
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return recovery{}, true, err
}
if fields == nil {
return recovery{}, true, errors.New("Shell recovery metadata must be a JSON object")
}
var value recovery
if err := json.Unmarshal(data, &value); err != nil {
return recovery{}, true, err
}
if err := validateRecovery(value, fields); err != nil {
return recovery{}, true, err
}
return value, true, nil
}
func validateRecovery(value recovery, fields map[string]json.RawMessage) error {
for _, name := range []string{"version", "original", "fingerprint"} {
if _, present := fields[name]; !present {
return fmt.Errorf("Shell recovery metadata is missing required field %q", name)
}
}
if value.Version != shellRecoveryVersion {
return fmt.Errorf("unsupported Shell recovery metadata version %d", value.Version)
}
decoded, err := hex.DecodeString(value.Fingerprint)
if err != nil || len(decoded) != sha256.Size {
return errors.New("Shell recovery metadata has an invalid fingerprint")
}
_, hasOriginalScript := fields["originalScript"]
if hasOriginalScript {
if value.OriginalScript == nil || value.Original != nil || len(*value.OriginalScript) == 0 || !recognizesProxy([]byte(*value.OriginalScript)) {
return errors.New("Shell recovery metadata has inconsistent original script state")
}
return nil
}
if value.Original == nil {
return errors.New("Shell recovery metadata is missing the original environment snapshot")
}
for name := range value.Original {
known := false
for _, candidate := range proxyVariables {
known = known || name == candidate
}
if !known {
return fmt.Errorf("Shell recovery metadata contains unsupported variable %q", name)
}
}
return nil
}
func (a *Adapter) ownership() *Ownership {
if !a.configuration.OwnFiles {
return nil
+37
View File
@@ -77,6 +77,43 @@ func TestShellAdapterRunsReusableTargetContract(t *testing.T) {
}
}
func TestIncompleteRecoveryMetadataRejectsRestoreWithoutChangingShellConfiguration(t *testing.T) {
fingerprint := strings.Repeat("a", 64)
cases := map[string]string{
"empty object": `{}`,
"missing restoration": `{"version":1,"fingerprint":"` + fingerprint + `","original":null}`,
"invalid fingerprint": `{"version":1,"fingerprint":"short","original":{}}`,
"conflicting originals": `{"version":1,"fingerprint":"` + fingerprint + `","original":{"HTTP_PROXY":"http://prior.example:3128"},"originalScript":"export HTTP_PROXY=http://prior.example:3128\\n"}`,
}
for name, metadata := range cases {
t.Run(name, func(t *testing.T) {
a := newAdapter(t, "/bin/bash", nil)
environment := []byte("export HTTP_PROXY='http://managed.example:8080'\n")
for path, data := range map[string][]byte{a.EnvironmentPath(): environment, a.RecoveryPath(): []byte(metadata)} {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
}
if inspection := a.Inspect(context.Background(), nil); inspection.State != target.Drifted {
t.Fatalf("inspection = %#v", inspection)
}
result := a.SetDesired(context.Background(), false, nil)
if result.Err == nil || result.Inspection.State != target.Drifted {
t.Fatalf("unsafe disable = %#v", result)
}
if got := readFile(t, a.EnvironmentPath()); got != string(environment) {
t.Fatalf("environment changed to %q", got)
}
if got := readFile(t, a.RecoveryPath()); got != metadata {
t.Fatalf("recovery changed to %q", got)
}
})
}
}
func TestRecoveryFailureRetryPreservesOriginalShellState(t *testing.T) {
files := &failingFS{FileSystem: shell.OSFileSystem{}, failRecoveryOnce: true}
a := newAdapterWithFS(t, "/bin/bash", map[string]string{"HTTP_PROXY": "http://prior.example:3128"}, files)