From 3fd30bb9e6cc76f03d2815db46fe0065a490d071 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 15 Feb 2026 14:03:28 -0800 Subject: [PATCH 01/10] Validate secret name in GetSecretVersion to prevent path traversal Add isValidSecretName() check at the top of GetSecretVersion(), matching the existing validation in AddSecret(). Without this, crafted secret names containing path traversal sequences (e.g. '../../../etc/passwd') could be used to read files outside the vault directory. Add regression tests for both GetSecretVersion and GetSecret. Closes #13 --- internal/vault/path_traversal_test.go | 66 +++++++++++++++++++++++++++ internal/vault/secrets.go | 6 +++ 2 files changed, 72 insertions(+) create mode 100644 internal/vault/path_traversal_test.go diff --git a/internal/vault/path_traversal_test.go b/internal/vault/path_traversal_test.go new file mode 100644 index 0000000..499ba31 --- /dev/null +++ b/internal/vault/path_traversal_test.go @@ -0,0 +1,66 @@ +package vault + +import ( + "testing" + + "git.eeqj.de/sneak/secret/internal/secret" + "github.com/awnumar/memguard" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGetSecretVersionRejectsPathTraversal verifies that GetSecretVersion +// validates the secret name and rejects path traversal attempts. +// This is a regression test for https://git.eeqj.de/sneak/secret/issues/13 +func TestGetSecretVersionRejectsPathTraversal(t *testing.T) { + testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + t.Setenv(secret.EnvMnemonic, testMnemonic) + t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase") + + fs := afero.NewMemMapFs() + stateDir := "/test/state" + + vlt, err := CreateVault(fs, stateDir, "test-vault") + require.NoError(t, err) + + // Add a legitimate secret so the vault is set up + value := memguard.NewBufferFromBytes([]byte("legitimate-secret")) + err = vlt.AddSecret("legit", value, false) + require.NoError(t, err) + + // These names contain path traversal and should be rejected + maliciousNames := []string{ + "../../../etc/passwd", + "..%2f..%2fetc/passwd", + ".secret", + "../sibling-vault/secrets.d/target", + } + + for _, name := range maliciousNames { + t.Run(name, func(t *testing.T) { + _, err := vlt.GetSecretVersion(name, "") + assert.Error(t, err, "GetSecretVersion should reject malicious name: %s", name) + assert.Contains(t, err.Error(), "invalid secret name", + "error should indicate invalid name for: %s", name) + }) + } +} + +// TestGetSecretRejectsPathTraversal verifies GetSecret (which calls GetSecretVersion) +// also rejects path traversal names. +func TestGetSecretRejectsPathTraversal(t *testing.T) { + testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + t.Setenv(secret.EnvMnemonic, testMnemonic) + t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase") + + fs := afero.NewMemMapFs() + stateDir := "/test/state" + + vlt, err := CreateVault(fs, stateDir, "test-vault") + require.NoError(t, err) + + _, err = vlt.GetSecret("../../../etc/passwd") + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid secret name") +} diff --git a/internal/vault/secrets.go b/internal/vault/secrets.go index 3452e0d..a7b3387 100644 --- a/internal/vault/secrets.go +++ b/internal/vault/secrets.go @@ -319,6 +319,12 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) { slog.String("version", version), ) + // Validate secret name to prevent path traversal + if !isValidSecretName(name) { + secret.Debug("Invalid secret name provided", "secret_name", name) + return nil, fmt.Errorf("invalid secret name '%s': must match pattern [a-z0-9.\\-_/]+", name) + } + // Get vault directory vaultDir, err := v.GetDirectory() if err != nil { From 0307f230246186d9cdb7f85927419c6348bd2aa7 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 14:03:50 -0800 Subject: [PATCH 02/10] Allow uppercase letters in secret names (closes #2) The isValidSecretName() regex only allowed lowercase letters [a-z], rejecting valid secret names containing uppercase characters (e.g. AWS access key IDs). Changed regex from ^[a-z0-9\.\-\_\/]+$ to ^[a-zA-Z0-9\.\-\_\/]+$ and added tests for uppercase secret names in both vault and secret packages. --- internal/secret/secret_test.go | 7 +++-- internal/vault/secrets.go | 4 +-- internal/vault/secrets_name_test.go | 42 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 internal/vault/secrets_name_test.go diff --git a/internal/secret/secret_test.go b/internal/secret/secret_test.go index c307d80..3639dd2 100644 --- a/internal/secret/secret_test.go +++ b/internal/secret/secret_test.go @@ -257,9 +257,10 @@ func isValidSecretName(name string) bool { if name == "" { return false } - // Valid characters for secret names: lowercase letters, numbers, dash, dot, underscore, slash + // Valid characters for secret names: letters, numbers, dash, dot, underscore, slash for _, char := range name { if (char < 'a' || char > 'z') && // lowercase letters + (char < 'A' || char > 'Z') && // uppercase letters (char < '0' || char > '9') && // numbers char != '-' && // dash char != '.' && // dot @@ -283,7 +284,9 @@ func TestSecretNameValidation(t *testing.T) { {"valid/path/name", true}, {"123valid", true}, {"", false}, - {"Invalid-Name", false}, // uppercase not allowed + {"Valid-Upper-Name", true}, // uppercase allowed + {"2025-11-21-ber1app1-vaultik-test-bucket-AKI", true}, // real-world uppercase key ID + {"MixedCase/Path/Name", true}, // mixed case with path {"invalid name", false}, // space not allowed {"invalid@name", false}, // @ not allowed } diff --git a/internal/vault/secrets.go b/internal/vault/secrets.go index 3452e0d..2c4c313 100644 --- a/internal/vault/secrets.go +++ b/internal/vault/secrets.go @@ -67,7 +67,7 @@ func (v *Vault) ListSecrets() ([]string, error) { return secrets, nil } -// isValidSecretName validates secret names according to the format [a-z0-9\.\-\_\/]+ +// isValidSecretName validates secret names according to the format [a-zA-Z0-9\.\-\_\/]+ // but with additional restrictions: // - No leading or trailing slashes // - No double slashes @@ -93,7 +93,7 @@ func isValidSecretName(name string) bool { } // Check the basic pattern - matched, _ := regexp.MatchString(`^[a-z0-9\.\-\_\/]+$`, name) + matched, _ := regexp.MatchString(`^[a-zA-Z0-9\.\-\_\/]+$`, name) return matched } diff --git a/internal/vault/secrets_name_test.go b/internal/vault/secrets_name_test.go new file mode 100644 index 0000000..205f8f3 --- /dev/null +++ b/internal/vault/secrets_name_test.go @@ -0,0 +1,42 @@ +package vault + +import "testing" + +func TestIsValidSecretNameUppercase(t *testing.T) { + tests := []struct { + name string + valid bool + }{ + // Lowercase (existing behavior) + {"valid-name", true}, + {"valid.name", true}, + {"valid_name", true}, + {"valid/path/name", true}, + {"123valid", true}, + + // Uppercase (new behavior - issue #2) + {"Valid-Upper-Name", true}, + {"2025-11-21-ber1app1-vaultik-test-bucket-AKI", true}, + {"MixedCase/Path/Name", true}, + {"ALLUPPERCASE", true}, + {"ABC123", true}, + + // Still invalid + {"", false}, + {"invalid name", false}, + {"invalid@name", false}, + {".dotstart", false}, + {"/leading-slash", false}, + {"trailing-slash/", false}, + {"double//slash", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isValidSecretName(tt.name) + if result != tt.valid { + t.Errorf("isValidSecretName(%q) = %v, want %v", tt.name, result, tt.valid) + } + }) + } +} From 8eb25b98fd3f213c3539856414f885d76a9d03b1 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 14:17:33 -0800 Subject: [PATCH 03/10] fix: block .. path components in secret names and validate in GetSecretObject - isValidSecretName() now rejects names with '..' path components (e.g. foo/../bar) - GetSecretObject() now calls isValidSecretName() before building paths - Added test cases for mid-path traversal patterns --- internal/vault/path_traversal_test.go | 30 +++++++++++++++++++++++++++ internal/vault/secrets.go | 11 ++++++++++ 2 files changed, 41 insertions(+) diff --git a/internal/vault/path_traversal_test.go b/internal/vault/path_traversal_test.go index 499ba31..f244fe1 100644 --- a/internal/vault/path_traversal_test.go +++ b/internal/vault/path_traversal_test.go @@ -35,6 +35,8 @@ func TestGetSecretVersionRejectsPathTraversal(t *testing.T) { "..%2f..%2fetc/passwd", ".secret", "../sibling-vault/secrets.d/target", + "foo/../bar", + "a/../../etc/passwd", } for _, name := range maliciousNames { @@ -64,3 +66,31 @@ func TestGetSecretRejectsPathTraversal(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "invalid secret name") } + +// TestGetSecretObjectRejectsPathTraversal verifies GetSecretObject +// also validates names and rejects path traversal attempts. +func TestGetSecretObjectRejectsPathTraversal(t *testing.T) { + testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + t.Setenv(secret.EnvMnemonic, testMnemonic) + t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase") + + fs := afero.NewMemMapFs() + stateDir := "/test/state" + + vlt, err := CreateVault(fs, stateDir, "test-vault") + require.NoError(t, err) + + maliciousNames := []string{ + "../../../etc/passwd", + "foo/../bar", + "a/../../etc/passwd", + } + + for _, name := range maliciousNames { + t.Run(name, func(t *testing.T) { + _, err := vlt.GetSecretObject(name) + assert.Error(t, err, "GetSecretObject should reject: %s", name) + assert.Contains(t, err.Error(), "invalid secret name") + }) + } +} diff --git a/internal/vault/secrets.go b/internal/vault/secrets.go index a7b3387..47b5b76 100644 --- a/internal/vault/secrets.go +++ b/internal/vault/secrets.go @@ -92,6 +92,13 @@ func isValidSecretName(name string) bool { return false } + // Check for path traversal via ".." components + for _, part := range strings.Split(name, "/") { + if part == ".." { + return false + } + } + // Check the basic pattern matched, _ := regexp.MatchString(`^[a-z0-9\.\-\_\/]+$`, name) @@ -460,6 +467,10 @@ func (v *Vault) UnlockVault() (*age.X25519Identity, error) { // GetSecretObject retrieves a Secret object with metadata loaded from this vault func (v *Vault) GetSecretObject(name string) (*secret.Secret, error) { + if !isValidSecretName(name) { + return nil, fmt.Errorf("invalid secret name: %s", name) + } + // First check if the secret exists by checking for the metadata file vaultDir, err := v.GetDirectory() if err != nil { From 4f984cd9c6db5cb62066372bad74e2bae6508a4f Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Feb 2026 23:41:43 -0800 Subject: [PATCH 04/10] fix: suppress gosec G204 for validated GPG key ID inputs --- internal/secret/pgpunlocker.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/secret/pgpunlocker.go b/internal/secret/pgpunlocker.go index 1fedb17..bca1546 100644 --- a/internal/secret/pgpunlocker.go +++ b/internal/secret/pgpunlocker.go @@ -320,7 +320,9 @@ func ResolveGPGKeyFingerprint(keyID string) (string, error) { } // Use GPG to get the full fingerprint for the key - cmd := exec.Command("gpg", "--list-keys", "--with-colons", "--fingerprint", keyID) + cmd := exec.Command( // #nosec G204 -- keyID validated + "gpg", "--list-keys", "--with-colons", "--fingerprint", keyID, + ) output, err := cmd.Output() if err != nil { return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err) @@ -359,7 +361,9 @@ func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error return nil, fmt.Errorf("invalid GPG key ID: %w", err) } - cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID) + cmd := exec.Command( // #nosec G204 -- keyID validated + "gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID, + ) cmd.Stdin = strings.NewReader(data.String()) output, err := cmd.Output() From e8339f4d120b62595c922236f499d840ca4477e6 Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Feb 2026 23:42:39 -0800 Subject: [PATCH 05/10] fix: update integration test to allow uppercase secret names --- internal/cli/integration_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/cli/integration_test.go b/internal/cli/integration_test.go index 66aea95..d995e58 100644 --- a/internal/cli/integration_test.go +++ b/internal/cli/integration_test.go @@ -1047,7 +1047,6 @@ func test12SecretNameFormats(t *testing.T, tempDir, testMnemonic string, runSecr // Test invalid secret names invalidNames := []string{ "", // empty - "UPPERCASE", // uppercase not allowed "with space", // spaces not allowed "with@symbol", // special characters not allowed "with#hash", // special characters not allowed @@ -1073,7 +1072,7 @@ func test12SecretNameFormats(t *testing.T, tempDir, testMnemonic string, runSecr // Some of these might not be invalid after all (e.g., leading/trailing slashes might be stripped, .hidden might be allowed) // For now, just check the ones we know should definitely fail - definitelyInvalid := []string{"", "UPPERCASE", "with space", "with@symbol", "with#hash", "with$dollar"} + definitelyInvalid := []string{"", "with space", "with@symbol", "with#hash", "with$dollar"} shouldFail := false for _, invalid := range definitelyInvalid { if invalidName == invalid { From 09ec79c57e9a07bd1ffefb3db4939f183635be21 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 8 Feb 2026 12:03:06 -0800 Subject: [PATCH 06/10] fix: use vault derivation index in getLongTermPrivateKey instead of hardcoded 0 Previously, getLongTermPrivateKey() always used derivation index 0 when deriving the long-term key from a mnemonic. This caused wrong key derivation for vaults with index > 0 (second+ vault from same mnemonic), leading to silent data corruption in keychain unlocker creation. Now reads the vault's actual DerivationIndex from vault-metadata.json. --- internal/secret/keychainunlocker.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/secret/keychainunlocker.go b/internal/secret/keychainunlocker.go index c19705a..c544214 100644 --- a/internal/secret/keychainunlocker.go +++ b/internal/secret/keychainunlocker.go @@ -251,8 +251,25 @@ func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) (*memguard.LockedB // Check if mnemonic is available in environment variable envMnemonic := os.Getenv(EnvMnemonic) if envMnemonic != "" { - // Use mnemonic directly to derive long-term key - ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0) + // Read vault metadata to get the correct derivation index + vaultDir, err := vault.GetDirectory() + if err != nil { + return nil, fmt.Errorf("failed to get vault directory: %w", err) + } + + metadataPath := filepath.Join(vaultDir, "vault-metadata.json") + metadataBytes, err := afero.ReadFile(fs, metadataPath) + if err != nil { + return nil, fmt.Errorf("failed to read vault metadata: %w", err) + } + + var metadata VaultMetadata + if err := json.Unmarshal(metadataBytes, &metadata); err != nil { + return nil, fmt.Errorf("failed to parse vault metadata: %w", err) + } + + // Use mnemonic with the vault's actual derivation index + ltIdentity, err := agehd.DeriveIdentity(envMnemonic, metadata.DerivationIndex) if err != nil { return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err) } From 0aa9a52497958ec2114df3ac63d304ac7b34049a Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 8 Feb 2026 17:21:31 -0800 Subject: [PATCH 07/10] test: add test for getLongTermPrivateKey derivation index Verifies that getLongTermPrivateKey reads the derivation index from vault metadata instead of using hardcoded index 0. Test creates a mock vault with DerivationIndex=5 and confirms the derived key matches index 5. --- internal/secret/derivation_index_test.go | 82 ++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 internal/secret/derivation_index_test.go diff --git a/internal/secret/derivation_index_test.go b/internal/secret/derivation_index_test.go new file mode 100644 index 0000000..ad86553 --- /dev/null +++ b/internal/secret/derivation_index_test.go @@ -0,0 +1,82 @@ +package secret + +import ( + "encoding/json" + "path/filepath" + "testing" + "time" + + "git.eeqj.de/sneak/secret/pkg/agehd" + "github.com/awnumar/memguard" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// realVault is a minimal VaultInterface backed by a real afero filesystem, +// using the same directory layout as vault.Vault. +type realVault struct { + name string + stateDir string + fs afero.Fs +} + +func (v *realVault) GetDirectory() (string, error) { + return filepath.Join(v.stateDir, "vaults.d", v.name), nil +} +func (v *realVault) GetName() string { return v.name } +func (v *realVault) GetFilesystem() afero.Fs { return v.fs } + +// Unused by getLongTermPrivateKey — these satisfy VaultInterface. +func (v *realVault) AddSecret(string, *memguard.LockedBuffer, bool) error { panic("not used") } +func (v *realVault) GetCurrentUnlocker() (Unlocker, error) { panic("not used") } +func (v *realVault) CreatePassphraseUnlocker(*memguard.LockedBuffer) (*PassphraseUnlocker, error) { + panic("not used") +} + +// createRealVault sets up a complete vault directory structure on an in-memory +// filesystem, identical to what vault.CreateVault produces. +func createRealVault(t *testing.T, fs afero.Fs, stateDir, name string, derivationIndex uint32) *realVault { + t.Helper() + + vaultDir := filepath.Join(stateDir, "vaults.d", name) + require.NoError(t, fs.MkdirAll(filepath.Join(vaultDir, "secrets.d"), DirPerms)) + require.NoError(t, fs.MkdirAll(filepath.Join(vaultDir, "unlockers.d"), DirPerms)) + + metadata := VaultMetadata{ + CreatedAt: time.Now(), + DerivationIndex: derivationIndex, + } + metaBytes, err := json.Marshal(metadata) + require.NoError(t, err) + require.NoError(t, afero.WriteFile(fs, filepath.Join(vaultDir, "vault-metadata.json"), metaBytes, FilePerms)) + + return &realVault{name: name, stateDir: stateDir, fs: fs} +} + +func TestGetLongTermPrivateKeyUsesVaultDerivationIndex(t *testing.T) { + const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + // Derive expected keys at two different indices to prove they differ. + key0, err := agehd.DeriveIdentity(testMnemonic, 0) + require.NoError(t, err) + key5, err := agehd.DeriveIdentity(testMnemonic, 5) + require.NoError(t, err) + require.NotEqual(t, key0.String(), key5.String(), + "sanity check: different derivation indices must produce different keys") + + // Build a real vault with DerivationIndex=5 on an in-memory filesystem. + fs := afero.NewMemMapFs() + vault := createRealVault(t, fs, "/state", "test-vault", 5) + + t.Setenv(EnvMnemonic, testMnemonic) + + result, err := getLongTermPrivateKey(fs, vault) + require.NoError(t, err) + defer result.Destroy() + + assert.Equal(t, key5.String(), string(result.Bytes()), + "getLongTermPrivateKey should derive at vault's DerivationIndex (5)") + assert.NotEqual(t, key0.String(), string(result.Bytes()), + "getLongTermPrivateKey must not use hardcoded index 0") +} From 596027f2107fd44939c618d3c0e7012ff87a3e7c Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Feb 2026 23:43:13 -0800 Subject: [PATCH 08/10] fix: suppress gosec G204 for validated GPG key ID inputs --- internal/secret/pgpunlocker.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/secret/pgpunlocker.go b/internal/secret/pgpunlocker.go index 1fedb17..bca1546 100644 --- a/internal/secret/pgpunlocker.go +++ b/internal/secret/pgpunlocker.go @@ -320,7 +320,9 @@ func ResolveGPGKeyFingerprint(keyID string) (string, error) { } // Use GPG to get the full fingerprint for the key - cmd := exec.Command("gpg", "--list-keys", "--with-colons", "--fingerprint", keyID) + cmd := exec.Command( // #nosec G204 -- keyID validated + "gpg", "--list-keys", "--with-colons", "--fingerprint", keyID, + ) output, err := cmd.Output() if err != nil { return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err) @@ -359,7 +361,9 @@ func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error return nil, fmt.Errorf("invalid GPG key ID: %w", err) } - cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID) + cmd := exec.Command( // #nosec G204 -- keyID validated + "gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID, + ) cmd.Stdin = strings.NewReader(data.String()) output, err := cmd.Output() From 6acd57d0ec31dce6fc51e02e6b28c177b05d868c Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Feb 2026 23:43:32 -0800 Subject: [PATCH 09/10] fix: suppress gosec G204 for validated GPG key ID inputs --- internal/secret/pgpunlocker.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/secret/pgpunlocker.go b/internal/secret/pgpunlocker.go index 1fedb17..bca1546 100644 --- a/internal/secret/pgpunlocker.go +++ b/internal/secret/pgpunlocker.go @@ -320,7 +320,9 @@ func ResolveGPGKeyFingerprint(keyID string) (string, error) { } // Use GPG to get the full fingerprint for the key - cmd := exec.Command("gpg", "--list-keys", "--with-colons", "--fingerprint", keyID) + cmd := exec.Command( // #nosec G204 -- keyID validated + "gpg", "--list-keys", "--with-colons", "--fingerprint", keyID, + ) output, err := cmd.Output() if err != nil { return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err) @@ -359,7 +361,9 @@ func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error return nil, fmt.Errorf("invalid GPG key ID: %w", err) } - cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID) + cmd := exec.Command( // #nosec G204 -- keyID validated + "gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID, + ) cmd.Stdin = strings.NewReader(data.String()) output, err := cmd.Output() From dc225bd0b1248587540e47f4505a20dbf9fede80 Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Feb 2026 23:44:38 -0800 Subject: [PATCH 10/10] fix: add blank line before return for nlreturn linter --- internal/vault/secrets.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/vault/secrets.go b/internal/vault/secrets.go index 47b5b76..1655982 100644 --- a/internal/vault/secrets.go +++ b/internal/vault/secrets.go @@ -329,6 +329,7 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) { // Validate secret name to prevent path traversal if !isValidSecretName(name) { secret.Debug("Invalid secret name provided", "secret_name", name) + return nil, fmt.Errorf("invalid secret name '%s': must match pattern [a-z0-9.\\-_/]+", name) }