From 8061c4c91445e9caf7340e05859ea98dffaaf6be Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 7 Apr 2026 23:08:35 +0800 Subject: [PATCH 1/5] feat: linux support custom data dir via environment variable --- internal/keychain/keychain_other.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/keychain/keychain_other.go b/internal/keychain/keychain_other.go index d84ad84b9..e369bccc6 100644 --- a/internal/keychain/keychain_other.go +++ b/internal/keychain/keychain_other.go @@ -25,6 +25,9 @@ const tagBytes = 16 // StorageDir returns the directory where encrypted files are stored. func StorageDir(service string) string { + if dir := os.Getenv("LARKSUITE_CLI_DATA_DIR"); dir != "" { + return dir + } home, err := vfs.UserHomeDir() if err != nil || home == "" { // If home is missing, fallback to relative path and print warning. From f567cc9ad30e7dd1c799586733193179297764dc Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Tue, 7 Apr 2026 23:16:43 +0800 Subject: [PATCH 2/5] feat(keychain): support custom log directory via LARKSUITE_CLI_LOG_DIR --- internal/keychain/auth_log.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/keychain/auth_log.go b/internal/keychain/auth_log.go index 079f8cd90..12b315154 100644 --- a/internal/keychain/auth_log.go +++ b/internal/keychain/auth_log.go @@ -21,6 +21,10 @@ var ( ) func authLogDir() string { + if dir := os.Getenv("LARKSUITE_CLI_LOG_DIR"); dir != "" { + return dir + } + if dir := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); dir != "" { return filepath.Join(dir, "logs") } From 4cb994a95ad326cb65e921930f7953d6d2ecf216 Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Wed, 8 Apr 2026 09:20:14 +0800 Subject: [PATCH 3/5] feat(security): validate env dir paths for security Add validation for environment variable directory paths to ensure they are absolute and safe. This prevents potential security issues from malformed paths. Also add corresponding tests to verify the validation behavior. --- internal/keychain/auth_log.go | 7 ++++- internal/keychain/auth_log_test.go | 31 ++++++++++++++++++++++ internal/keychain/keychain_other.go | 7 ++++- internal/keychain/keychain_other_test.go | 33 ++++++++++++++++++++++++ internal/validate/path.go | 17 ++++++++++++ internal/validate/path_test.go | 23 +++++++++++++++++ 6 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 internal/keychain/auth_log_test.go create mode 100644 internal/keychain/keychain_other_test.go diff --git a/internal/keychain/auth_log.go b/internal/keychain/auth_log.go index 12b315154..d15c72943 100644 --- a/internal/keychain/auth_log.go +++ b/internal/keychain/auth_log.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/internal/vfs" ) @@ -22,7 +23,11 @@ var ( func authLogDir() string { if dir := os.Getenv("LARKSUITE_CLI_LOG_DIR"); dir != "" { - return dir + safeDir, err := validate.SafeEnvDirPath(dir, "LARKSUITE_CLI_LOG_DIR") + if err == nil { + return safeDir + } + fmt.Fprintf(os.Stderr, "warning: ignoring invalid LARKSUITE_CLI_LOG_DIR: %v\n", err) } if dir := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); dir != "" { diff --git a/internal/keychain/auth_log_test.go b/internal/keychain/auth_log_test.go new file mode 100644 index 000000000..7276d0ae8 --- /dev/null +++ b/internal/keychain/auth_log_test.go @@ -0,0 +1,31 @@ +package keychain + +import ( + "path/filepath" + "testing" +) + +func TestAuthLogDir_UsesValidatedLogDirEnv(t *testing.T) { + base := t.TempDir() + base, _ = filepath.EvalSymlinks(base) + t.Setenv("LARKSUITE_CLI_LOG_DIR", filepath.Join(base, "logs", "..", "auth")) + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", "") + + got := authLogDir() + want := filepath.Join(base, "auth") + if got != want { + t.Fatalf("authLogDir() = %q, want %q", got, want) + } +} + +func TestAuthLogDir_InvalidLogDirFallsBackToConfigDir(t *testing.T) { + t.Setenv("LARKSUITE_CLI_LOG_DIR", "relative-logs") + configDir := t.TempDir() + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir) + + got := authLogDir() + want := filepath.Join(configDir, "logs") + if got != want { + t.Fatalf("authLogDir() = %q, want %q", got, want) + } +} diff --git a/internal/keychain/keychain_other.go b/internal/keychain/keychain_other.go index e369bccc6..388741695 100644 --- a/internal/keychain/keychain_other.go +++ b/internal/keychain/keychain_other.go @@ -16,6 +16,7 @@ import ( "regexp" "github.com/google/uuid" + "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/internal/vfs" ) @@ -26,7 +27,11 @@ const tagBytes = 16 // StorageDir returns the directory where encrypted files are stored. func StorageDir(service string) string { if dir := os.Getenv("LARKSUITE_CLI_DATA_DIR"); dir != "" { - return dir + safeDir, err := validate.SafeEnvDirPath(dir, "LARKSUITE_CLI_DATA_DIR") + if err == nil { + return filepath.Join(safeDir, service) + } + fmt.Fprintf(os.Stderr, "warning: ignoring invalid LARKSUITE_CLI_DATA_DIR: %v\n", err) } home, err := vfs.UserHomeDir() if err != nil || home == "" { diff --git a/internal/keychain/keychain_other_test.go b/internal/keychain/keychain_other_test.go new file mode 100644 index 000000000..5abad7f2b --- /dev/null +++ b/internal/keychain/keychain_other_test.go @@ -0,0 +1,33 @@ +//go:build linux + +package keychain + +import ( + "path/filepath" + "testing" +) + +func TestStorageDir_UsesValidatedDataDirEnv(t *testing.T) { + base := t.TempDir() + base, _ = filepath.EvalSymlinks(base) + t.Setenv("LARKSUITE_CLI_DATA_DIR", filepath.Join(base, "data", "..", "store")) + + got := StorageDir("svc") + want := filepath.Join(base, "store", "svc") + if got != want { + t.Fatalf("StorageDir() = %q, want %q", got, want) + } +} + +func TestStorageDir_InvalidDataDirFallsBackToDefault(t *testing.T) { + home := t.TempDir() + home, _ = filepath.EvalSymlinks(home) + t.Setenv("LARKSUITE_CLI_DATA_DIR", "relative-data") + t.Setenv("HOME", home) + + got := StorageDir("svc") + want := filepath.Join(home, ".local", "share", "svc") + if got != want { + t.Fatalf("StorageDir() = %q, want %q", got, want) + } +} diff --git a/internal/validate/path.go b/internal/validate/path.go index ecc6e973e..39d416547 100644 --- a/internal/validate/path.go +++ b/internal/validate/path.go @@ -32,6 +32,23 @@ func SafeInputPath(path string) (string, error) { return safePath(path, "--file") } +func SafeEnvDirPath(path, envName string) (string, error) { + if err := RejectControlChars(path, envName); err != nil { + return "", err + } + + path = filepath.Clean(path) + if !filepath.IsAbs(path) { + return "", fmt.Errorf("%s must be an absolute path, got %q", envName, path) + } + + resolved, err := resolveNearestAncestor(path) + if err != nil { + return "", fmt.Errorf("cannot resolve symlinks: %w", err) + } + return resolved, nil +} + // SafeLocalFlagPath validates a flag value as a local file path. // Empty values and http/https URLs are returned unchanged without validation, // allowing the caller to handle non-path inputs (e.g. API keys, URLs) upstream. diff --git a/internal/validate/path_test.go b/internal/validate/path_test.go index bc6b1f485..233fb1def 100644 --- a/internal/validate/path_test.go +++ b/internal/validate/path_test.go @@ -283,3 +283,26 @@ func TestSafeInputPath_ErrorMessageContainsCorrectFlagName(t *testing.T) { t.Errorf("error should mention --output, got: %s", err.Error()) } } + +func TestSafeEnvDirPath_RequiresAbsolutePath(t *testing.T) { + _, err := SafeEnvDirPath("logs", "LARKSUITE_CLI_LOG_DIR") + if err == nil { + t.Fatal("expected error for relative path") + } + if !strings.Contains(err.Error(), "LARKSUITE_CLI_LOG_DIR") { + t.Fatalf("error should mention env name, got %v", err) + } +} + +func TestSafeEnvDirPath_ReturnsNormalizedAbsolutePath(t *testing.T) { + base := t.TempDir() + base, _ = filepath.EvalSymlinks(base) + got, err := SafeEnvDirPath(filepath.Join(base, "logs", "..", "auth"), "LARKSUITE_CLI_LOG_DIR") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := filepath.Join(base, "auth") + if got != want { + t.Fatalf("SafeEnvDirPath() = %q, want %q", got, want) + } +} From 4406b7497fdd129ba3e54cfb5b8b0fe7cfe18410 Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Wed, 8 Apr 2026 09:32:49 +0800 Subject: [PATCH 4/5] docs(validate): add function and test documentation comments Add missing documentation comments for SafeEnvDirPath function and related test cases to improve code clarity and maintainability --- internal/keychain/auth_log_test.go | 4 ++++ internal/keychain/keychain_other_test.go | 4 ++++ internal/validate/path.go | 4 ++++ internal/validate/path_test.go | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/internal/keychain/auth_log_test.go b/internal/keychain/auth_log_test.go index 7276d0ae8..c81fc1ff9 100644 --- a/internal/keychain/auth_log_test.go +++ b/internal/keychain/auth_log_test.go @@ -5,6 +5,8 @@ import ( "testing" ) +// TestAuthLogDir_UsesValidatedLogDirEnv verifies that a valid absolute +// LARKSUITE_CLI_LOG_DIR is normalized and used as the auth log directory. func TestAuthLogDir_UsesValidatedLogDirEnv(t *testing.T) { base := t.TempDir() base, _ = filepath.EvalSymlinks(base) @@ -18,6 +20,8 @@ func TestAuthLogDir_UsesValidatedLogDirEnv(t *testing.T) { } } +// TestAuthLogDir_InvalidLogDirFallsBackToConfigDir verifies that an invalid +// LARKSUITE_CLI_LOG_DIR falls back to LARKSUITE_CLI_CONFIG_DIR/logs. func TestAuthLogDir_InvalidLogDirFallsBackToConfigDir(t *testing.T) { t.Setenv("LARKSUITE_CLI_LOG_DIR", "relative-logs") configDir := t.TempDir() diff --git a/internal/keychain/keychain_other_test.go b/internal/keychain/keychain_other_test.go index 5abad7f2b..a07cd8811 100644 --- a/internal/keychain/keychain_other_test.go +++ b/internal/keychain/keychain_other_test.go @@ -7,6 +7,8 @@ import ( "testing" ) +// TestStorageDir_UsesValidatedDataDirEnv verifies that a valid absolute +// LARKSUITE_CLI_DATA_DIR is normalized and still preserves service isolation. func TestStorageDir_UsesValidatedDataDirEnv(t *testing.T) { base := t.TempDir() base, _ = filepath.EvalSymlinks(base) @@ -19,6 +21,8 @@ func TestStorageDir_UsesValidatedDataDirEnv(t *testing.T) { } } +// TestStorageDir_InvalidDataDirFallsBackToDefault verifies that an invalid +// LARKSUITE_CLI_DATA_DIR falls back to the default per-service storage path. func TestStorageDir_InvalidDataDirFallsBackToDefault(t *testing.T) { home := t.TempDir() home, _ = filepath.EvalSymlinks(home) diff --git a/internal/validate/path.go b/internal/validate/path.go index 39d416547..d46f3571d 100644 --- a/internal/validate/path.go +++ b/internal/validate/path.go @@ -32,6 +32,10 @@ func SafeInputPath(path string) (string, error) { return safePath(path, "--file") } +// SafeEnvDirPath validates an environment-provided application directory path. +// It requires an absolute path, rejects control characters, normalizes the +// input, and resolves symlinks through the nearest existing ancestor so callers +// receive a canonical path for subsequent filesystem operations. func SafeEnvDirPath(path, envName string) (string, error) { if err := RejectControlChars(path, envName); err != nil { return "", err diff --git a/internal/validate/path_test.go b/internal/validate/path_test.go index 233fb1def..b99a16a85 100644 --- a/internal/validate/path_test.go +++ b/internal/validate/path_test.go @@ -284,6 +284,8 @@ func TestSafeInputPath_ErrorMessageContainsCorrectFlagName(t *testing.T) { } } +// TestSafeEnvDirPath_RequiresAbsolutePath verifies that environment-provided +// directory paths must be absolute. func TestSafeEnvDirPath_RequiresAbsolutePath(t *testing.T) { _, err := SafeEnvDirPath("logs", "LARKSUITE_CLI_LOG_DIR") if err == nil { @@ -294,6 +296,8 @@ func TestSafeEnvDirPath_RequiresAbsolutePath(t *testing.T) { } } +// TestSafeEnvDirPath_ReturnsNormalizedAbsolutePath verifies that a valid +// absolute environment directory is cleaned and resolved to its canonical path. func TestSafeEnvDirPath_ReturnsNormalizedAbsolutePath(t *testing.T) { base := t.TempDir() base, _ = filepath.EvalSymlinks(base) From d009cba7e9ed0fae6a3831e3f9276256ee3c862a Mon Sep 17 00:00:00 2001 From: zhaojunchang Date: Wed, 8 Apr 2026 09:44:50 +0800 Subject: [PATCH 5/5] refactor(keychain): remove warning logs for invalid env vars --- internal/keychain/auth_log.go | 1 - internal/keychain/keychain_other.go | 1 - 2 files changed, 2 deletions(-) diff --git a/internal/keychain/auth_log.go b/internal/keychain/auth_log.go index d15c72943..8bc9e947a 100644 --- a/internal/keychain/auth_log.go +++ b/internal/keychain/auth_log.go @@ -27,7 +27,6 @@ func authLogDir() string { if err == nil { return safeDir } - fmt.Fprintf(os.Stderr, "warning: ignoring invalid LARKSUITE_CLI_LOG_DIR: %v\n", err) } if dir := os.Getenv("LARKSUITE_CLI_CONFIG_DIR"); dir != "" { diff --git a/internal/keychain/keychain_other.go b/internal/keychain/keychain_other.go index 388741695..d0d094dd5 100644 --- a/internal/keychain/keychain_other.go +++ b/internal/keychain/keychain_other.go @@ -31,7 +31,6 @@ func StorageDir(service string) string { if err == nil { return filepath.Join(safeDir, service) } - fmt.Fprintf(os.Stderr, "warning: ignoring invalid LARKSUITE_CLI_DATA_DIR: %v\n", err) } home, err := vfs.UserHomeDir() if err != nil || home == "" {