From 224b26803889489a4fcb6b887a74f18ba659e088 Mon Sep 17 00:00:00 2001 From: Niko Savola Date: Thu, 17 Sep 2026 15:21:35 +0300 Subject: [PATCH] fix: write the config file with owner-only permissions - the file holds the api token but was created 0644 via viper's default Co-Authored-By: DeepSeek V4.1 Flash --- CHANGELOG.md | 6 +++++ pkg/cmdutil/config.go | 12 +++++++-- pkg/cmdutil/config_save_test.go | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 pkg/cmdutil/config_save_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0acb723b..7a0616f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security + +- the config file, which stores the API token, is now saved with owner-only + permissions, and files left world-readable by older versions are tightened on + the next save + ## [v0.64.3] - 2026-09-17 ### Fixed diff --git a/pkg/cmdutil/config.go b/pkg/cmdutil/config.go index 0b6f6b63..a760c5b2 100644 --- a/pkg/cmdutil/config.go +++ b/pkg/cmdutil/config.go @@ -223,11 +223,19 @@ func (*config) Save() error { } dir := path.Join(home, ".config", "clockify-cli") - _ = os.MkdirAll(dir, os.ModePerm) + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } filename = path.Join(dir, ".clockify-cli.yaml") } - return viper.WriteConfigAs(filename) + // the file holds the api token, keep it owner-only even when it was + // created world-readable by an older version + viper.SetConfigPermissions(0o600) + if err := viper.WriteConfigAs(filename); err != nil { + return err + } + return os.Chmod(filename, 0o600) } func configFunc() func() (c Config) { diff --git a/pkg/cmdutil/config_save_test.go b/pkg/cmdutil/config_save_test.go new file mode 100644 index 00000000..bb87b355 --- /dev/null +++ b/pkg/cmdutil/config_save_test.go @@ -0,0 +1,46 @@ +package cmdutil_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/lucassabreu/clockify-cli/pkg/cmdutil" +) + +func TestSaveUsesOwnerOnlyPermissions(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Cleanup(homedir.Reset) + homedir.Reset() + t.Cleanup(func() { viper.SetConfigFile("") }) + viper.SetConfigFile("") + + config := cmdutil.NewFactory(cmdutil.Version{}).Config() + config.SetString(cmdutil.CONF_TOKEN, "a token") + require.NoError(t, config.Save()) + + dir := filepath.Join(home, ".config", "clockify-cli") + filename := filepath.Join(dir, ".clockify-cli.yaml") + + dirInfo, err := os.Stat(dir) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o700), dirInfo.Mode().Perm()) + + fileInfo, err := os.Stat(filename) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o600), fileInfo.Mode().Perm()) + + // a file left world-readable by an older version gets tightened on save + require.NoError(t, os.Chmod(filename, 0o644)) + require.NoError(t, config.Save()) + + fileInfo, err = os.Stat(filename) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o600), fileInfo.Mode().Perm()) +}