From 90fc4e315db23a38c99b795a373d72b4a9d5cfae Mon Sep 17 00:00:00 2001 From: Gadi Evron Date: Thu, 28 May 2026 12:56:02 -0700 Subject: [PATCH 1/2] fix(cli/config): enforce 0600 on the config file even when it pre-exists (CWE-732) config.Save() wrote the config (which may hold an API key) with os.WriteFile(path, data, 0600). os.WriteFile only applies the mode when it CREATES the file; if config.json already existed with looser permissions (e.g. 0644), the secret stayed world/group-readable after Save(). Enforce 0600 explicitly via os.Chmod after the write (CWE-732, insecure permissions for a secret-bearing file). Tests: internal/config/config_perms_test.go (a pre-existing 0644 secret config becomes 0600 after Save). RED 1 failed (0644) -> GREEN; go test ./internal/config/ ok; gofmt + go vet clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/openant-cli/internal/config/config.go | 6 +++ .../internal/config/config_perms_test.go | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 apps/openant-cli/internal/config/config_perms_test.go diff --git a/apps/openant-cli/internal/config/config.go b/apps/openant-cli/internal/config/config.go index 0af8f521..9171994c 100644 --- a/apps/openant-cli/internal/config/config.go +++ b/apps/openant-cli/internal/config/config.go @@ -107,6 +107,12 @@ func Save(cfg *Config) error { return fmt.Errorf("failed to write config: %w", err) } + // os.WriteFile only applies the mode when it CREATES the file; a pre-existing config + // may hold an API key under looser permissions, so enforce 0600 explicitly (CWE-732). + if err := os.Chmod(path, 0600); err != nil { + return fmt.Errorf("failed to restrict config permissions: %w", err) + } + return nil } diff --git a/apps/openant-cli/internal/config/config_perms_test.go b/apps/openant-cli/internal/config/config_perms_test.go new file mode 100644 index 00000000..80588bd1 --- /dev/null +++ b/apps/openant-cli/internal/config/config_perms_test.go @@ -0,0 +1,42 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +// CWE-732: Save() does not enforce restrictive +// permissions on a PRE-EXISTING config file. os.WriteFile only applies the mode argument +// when it creates the file; if the config (which may hold an API key) already exists with +// looser perms (e.g. 0644), the secret stays world-readable after Save(). +func TestSaveEnforcesRestrictivePermsOnPreexistingFile(t *testing.T) { + tmp := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tmp) + + dir := filepath.Join(tmp, "openant") + if err := os.MkdirAll(dir, 0700); err != nil { + t.Fatal(err) + } + path := filepath.Join(dir, "config.json") + + // Pre-existing config file with loose (world/group-readable) permissions. + if err := os.WriteFile(path, []byte(`{"api_key":"old"}`), 0644); err != nil { + t.Fatal(err) + } + if err := os.Chmod(path, 0644); err != nil { // defeat umask so the file is genuinely 0644 + t.Fatal(err) + } + + if err := Save(&Config{APIKey: "sk-secret-value"}); err != nil { + t.Fatalf("Save failed: %v", err) + } + + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if perm := info.Mode().Perm(); perm != 0600 { + t.Fatalf("secret config perms = %#o, want 0600 (CWE-732: pre-existing loose perms not enforced)", perm) + } +} From 011edde5ce74493b452f34c627340ef9a1261bdc Mon Sep 17 00:00:00 2001 From: Gadi Evron Date: Thu, 28 May 2026 17:25:53 -0700 Subject: [PATCH 2/2] test(cli/config): skip POSIX-perms test on Windows TestSaveEnforcesRestrictivePermsOnPreexistingFile asserts 0600 file-mode bits, which Windows does not enforce (got 0666). Guard the test with a runtime.GOOS == windows skip so the CWE-732 0600 enforcement check still runs on Unix. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/openant-cli/internal/config/config_perms_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/openant-cli/internal/config/config_perms_test.go b/apps/openant-cli/internal/config/config_perms_test.go index 80588bd1..f2bafa50 100644 --- a/apps/openant-cli/internal/config/config_perms_test.go +++ b/apps/openant-cli/internal/config/config_perms_test.go @@ -3,6 +3,7 @@ package config import ( "os" "path/filepath" + "runtime" "testing" ) @@ -11,6 +12,9 @@ import ( // when it creates the file; if the config (which may hold an API key) already exists with // looser perms (e.g. 0644), the secret stays world-readable after Save(). func TestSaveEnforcesRestrictivePermsOnPreexistingFile(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("POSIX file-mode bits not enforced on Windows") + } tmp := t.TempDir() t.Setenv("XDG_CONFIG_HOME", tmp)