Reject invalid secret names before any command builds a path (closes #33)
check / check (push) Successful in 49s
check / check (push) Successful in 49s
`secret rm ..` deleted the whole vault; `secret rm .` and `secret rm ""` deleted every secret. rm, mv, the version commands, encrypt and decrypt built paths from the name unchecked; import checked it only after reading the source file. Each now calls vault.ValidateSecretName, which wraps the existing rule, on the name as given, before building any path; MoveSecret checks both names before switching the current vault. Its error and README.md state the rule. The test-only copy of the rule in internal/secret is removed. The regression test runs each rejected command on a copy of two in-memory vaults and requires the exact error and an unchanged state directory. Model: opus-5-5
This commit is contained in:
+26
-12
@@ -79,6 +79,7 @@ func (v *Vault) ListSecrets() ([]string, error) {
|
||||
// - No leading or trailing slashes
|
||||
// - No double slashes
|
||||
// - No names starting with dots
|
||||
// - No ".." path segments
|
||||
func isValidSecretName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
@@ -110,6 +111,22 @@ func isValidSecretName(name string) bool {
|
||||
return matched
|
||||
}
|
||||
|
||||
// ValidateSecretName returns an error wrapping ErrInvalidSecretName when
|
||||
// name is not a valid secret name. Call it on the name exactly as the user
|
||||
// gave it, before building any path from it.
|
||||
func ValidateSecretName(name string) error {
|
||||
if !isValidSecretName(name) {
|
||||
return fmt.Errorf(
|
||||
"%w '%s': only ASCII letters, digits, '.', '-', '_' and '/' are allowed, "+
|
||||
"and a name must not be empty, start with '.' or '/', end with '/', "+
|
||||
"contain '//', or have '..' as a path segment",
|
||||
ErrInvalidSecretName, name,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSecret adds a secret to this vault
|
||||
func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool) error {
|
||||
if value == nil {
|
||||
@@ -124,13 +141,11 @@ func (v *Vault) AddSecret(name string, value *memguard.LockedBuffer, force bool)
|
||||
)
|
||||
|
||||
// Validate secret name
|
||||
if !isValidSecretName(name) {
|
||||
err := ValidateSecretName(name)
|
||||
if err != nil {
|
||||
secret.Debug("Invalid secret name provided", "secret_name", name)
|
||||
|
||||
return fmt.Errorf(
|
||||
"%w '%s': must match pattern [a-z0-9.\\-_/]+",
|
||||
ErrInvalidSecretName, name,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
secret.Debug("Secret name validation passed", "secret_name", name)
|
||||
@@ -358,8 +373,9 @@ 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("%w: %s", ErrInvalidSecretName, name)
|
||||
err := ValidateSecretName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First check if the secret exists by checking for the metadata file
|
||||
@@ -628,13 +644,11 @@ func (v *Vault) updatePreviousVersion(
|
||||
// version exist, and resolves an empty version to the current one.
|
||||
func (v *Vault) resolveSecretVersion(name, version string) (string, error) {
|
||||
// Validate secret name to prevent path traversal
|
||||
if !isValidSecretName(name) {
|
||||
err := ValidateSecretName(name)
|
||||
if err != nil {
|
||||
secret.Debug("Invalid secret name provided", "secret_name", name)
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"%w '%s': must match pattern [a-z0-9.\\-_/]+",
|
||||
ErrInvalidSecretName, name,
|
||||
)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get vault directory
|
||||
|
||||
Reference in New Issue
Block a user