Reject secret mv onto the same secret under another name (closes #78)
check / check (push) Successful in 1m3s
check / check (push) Successful in 1m3s
On a case-insensitive filesystem (the macOS default) "Foo" and "foo" name one secret, so `secret mv --force Foo foo` removed the destination, which was the source, and lost the secret with every version. Between vaults the copy replaced the source, and removing the source then removed the copy. Both kinds of move now compare the two secret directories with os.SameFile before changing anything and reject the move if they are one, with or without --force. The tests give one secret two names with symbolic links on the real filesystem. Model: opus-5-5
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -890,6 +891,18 @@ func (cli *Instance) moveSecretWithinVault(
|
||||
destEncoded := strings.ReplaceAll(dest, "/", "%")
|
||||
destDir := filepath.Join(vaultDir, "secrets.d", destEncoded)
|
||||
|
||||
// Removing a destination that is the source under another name, such as
|
||||
// "foo" for "Foo" on a case-insensitive filesystem, would delete it too.
|
||||
same, err := cli.sameDirectory(sourceDir, destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if same {
|
||||
return fmt.Errorf("secret '%s' %w: '%s' is the same secret on "+
|
||||
"this filesystem", source, errMoveOntoItself, dest)
|
||||
}
|
||||
|
||||
exists, err = afero.DirExists(cli.fs, destDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if destination secret exists: %w", err)
|
||||
@@ -916,6 +929,31 @@ func (cli *Instance) moveSecretWithinVault(
|
||||
return nil
|
||||
}
|
||||
|
||||
// sameDirectory reports whether the existing directory dir and the path
|
||||
// other are one directory under two names, as secrets.d/Foo and
|
||||
// secrets.d/foo are on a case-insensitive filesystem, or a directory and a
|
||||
// symbolic link to it. Removing other to make room for dir would then delete
|
||||
// dir. It is false if other does not exist, and always false on the
|
||||
// in-memory filesystem, which has no such aliasing and whose files
|
||||
// os.SameFile does not compare.
|
||||
func (cli *Instance) sameDirectory(dir, other string) (bool, error) {
|
||||
dirInfo, err := cli.fs.Stat(dir)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check %s: %w", dir, err)
|
||||
}
|
||||
|
||||
otherInfo, err := cli.fs.Stat(other)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check %s: %w", other, err)
|
||||
}
|
||||
|
||||
return os.SameFile(dirInfo, otherInfo), nil
|
||||
}
|
||||
|
||||
// moveSecretCrossVault handles moving between two different vaults. Its
|
||||
// caller, MoveSecret, has already checked both secret names and that both
|
||||
// vaults exist.
|
||||
@@ -940,6 +978,27 @@ func (cli *Instance) moveSecretCrossVault(
|
||||
srcSecretName, errSecretNotFound, srcVault.Name)
|
||||
}
|
||||
|
||||
// The source is removed after the copy, so a destination that is the
|
||||
// source under another name would be lost with it.
|
||||
destVaultDir, err := destVault.GetDirectory()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get destination vault directory: %w", err)
|
||||
}
|
||||
|
||||
destStorageName := strings.ReplaceAll(destSecretName, "/", "%")
|
||||
destSecretDir := filepath.Join(destVaultDir, "secrets.d", destStorageName)
|
||||
|
||||
same, err := cli.sameDirectory(srcSecretDir, destSecretDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if same {
|
||||
return fmt.Errorf("secret '%s:%s' %w: '%s:%s' is the same secret on "+
|
||||
"this filesystem", srcVault.Name, srcSecretName, errMoveOntoItself,
|
||||
destVault.Name, destSecretName)
|
||||
}
|
||||
|
||||
// Unlock destination vault (will fail if neither mnemonic nor unlocker available)
|
||||
_, err = destVault.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user