Harden CLI config persistence
This commit is contained in:
+41
-1
@@ -1801,7 +1801,47 @@ func writeConfig(path string, cfg cliConfig) error {
|
||||
if err != nil {
|
||||
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 {
|
||||
|
||||
@@ -156,6 +156,36 @@ func TestConfigSetRedactsSecrets(t *testing.T) {
|
||||
if !strings.Contains(string(raw), cliTestAPIKey) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user