Harden CLI config persistence

This commit is contained in:
Codex
2026-06-17 15:50:44 +02:00
parent f0605f1c3f
commit 5030782a22
2 changed files with 71 additions and 1 deletions
+41 -1
View File
@@ -1801,7 +1801,47 @@ func writeConfig(path string, cfg cliConfig) error {
if err != nil { if err != nil {
return err return err
} }
return os.WriteFile(path, append(raw, '\n'), 0o600) return writeFileAtomic0600(path, append(raw, '\n'))
}
func writeFileAtomic0600(path string, body []byte) error {
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-")
if err != nil {
return err
}
tmpName := tmp.Name()
removeTmp := true
defer func() {
if removeTmp {
_ = os.Remove(tmpName)
}
}()
if err := tmp.Chmod(0o600); err != nil {
_ = tmp.Close()
return err
}
written, err := tmp.Write(body)
if err != nil {
_ = tmp.Close()
return err
}
if written != len(body) {
_ = tmp.Close()
return fmt.Errorf("short atomic write: wrote %d of %d bytes", written, len(body))
}
if err := tmp.Sync(); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Rename(tmpName, path); err != nil {
return err
}
removeTmp = false
return nil
} }
func redactValue(value any) any { func redactValue(value any) any {
+30
View File
@@ -156,6 +156,36 @@ func TestConfigSetRedactsSecrets(t *testing.T) {
if !strings.Contains(string(raw), cliTestAPIKey) { if !strings.Contains(string(raw), cliTestAPIKey) {
t.Fatalf("config did not persist api key") t.Fatalf("config did not persist api key")
} }
info, err := os.Stat(config)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("config mode = %o, want 600", info.Mode().Perm())
}
matches, err := filepath.Glob(filepath.Join(dir, ".config.json.tmp-*"))
if err != nil {
t.Fatal(err)
}
if len(matches) != 0 {
t.Fatalf("atomic config temp files leaked: %v", matches)
}
}
func TestConfigSetFailsWhenConfigPathIsDirectory(t *testing.T) {
dir := t.TempDir()
env := testEnv(t, map[string]string{
"ATTESTO_CONFIG": dir,
"ATT_API_KEY": cliTestAPIKey,
})
var stdout, stderr bytes.Buffer
code := run([]string{"--json", "config", "set", "--api-key-env", "ATT_API_KEY"}, &stdout, &stderr, env)
if code == 0 {
t.Fatal("config set must fail when config path is a directory")
}
if strings.Contains(stdout.String(), cliTestAPIKey) || strings.Contains(stderr.String(), cliTestAPIKey) {
t.Fatalf("secret leaked on write failure: stdout=%s stderr=%s", stdout.String(), stderr.String())
}
} }
func TestLocalVaultSpoolAndStatus(t *testing.T) { func TestLocalVaultSpoolAndStatus(t *testing.T) {