diff options
author | Jakob Unterwurzacher | 2021-06-02 14:21:30 +0200 |
---|---|---|
committer | Jakob Unterwurzacher | 2021-06-02 14:29:48 +0200 |
commit | 04858ddd222bbf7156f33f99cfb293a9b1e15ec8 (patch) | |
tree | 732cbf83c9d842a911d515abbad7c153c4159354 /internal/nametransform | |
parent | 242cdf966f262b2e20785eb0ff49ac55a8bd4636 (diff) |
nametransform: check name validity on encryption
xfstests generic/523 discovered that we allowed to set
xattrs with "/" in the name, but did not allow to read
them later.
With this change we do not allow to set them in the first
place.
Diffstat (limited to 'internal/nametransform')
-rw-r--r-- | internal/nametransform/diriv.go | 5 | ||||
-rw-r--r-- | internal/nametransform/longnames.go | 5 | ||||
-rw-r--r-- | internal/nametransform/names.go | 27 | ||||
-rw-r--r-- | internal/nametransform/names_test.go | 24 | ||||
-rw-r--r-- | internal/nametransform/valid.go | 27 |
5 files changed, 70 insertions, 18 deletions
diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index b2f165b..1d27aa5 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -102,7 +102,10 @@ func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, err if len(name) > NameMax { return "", syscall.ENAMETOOLONG } - cName := be.EncryptName(name, iv) + cName, err := be.EncryptName(name, iv) + if err != nil { + return "", err + } if be.longNames && len(cName) > NameMax { return be.HashLongName(cName), nil } diff --git a/internal/nametransform/longnames.go b/internal/nametransform/longnames.go index aa463a1..74ddb07 100644 --- a/internal/nametransform/longnames.go +++ b/internal/nametransform/longnames.go @@ -132,7 +132,10 @@ func (n *NameTransform) WriteLongNameAt(dirfd int, hashName string, plainName st if err != nil { return err } - cName := n.EncryptName(plainName, dirIV) + cName, err := n.EncryptName(plainName, dirIV) + if err != nil { + return err + } // Write the encrypted name into hashName.name fdRaw, err := syscallcompat.Openat(dirfd, hashName+LongNameSuffix, diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 119d592..ca28230 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -2,7 +2,6 @@ package nametransform import ( - "bytes" "crypto/aes" "encoding/base64" "path/filepath" @@ -21,7 +20,7 @@ const ( // NameTransformer is an interface used to transform filenames. type NameTransformer interface { DecryptName(cipherName string, iv []byte) (string, error) - EncryptName(plainName string, iv []byte) string + EncryptName(plainName string, iv []byte) (string, error) EncryptAndHashName(name string, iv []byte) (string, error) // HashLongName - take the hash of a long string "name" and return // "gocryptfs.longname.[sha256]" @@ -99,22 +98,14 @@ func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error bin = n.emeCipher.Decrypt(iv, bin) bin, err = unPad16(bin) if err != nil { - tlog.Debug.Printf("DecryptName: unPad16 error detail: %v", err) - // unPad16 returns detailed errors including the position of the - // incorrect bytes. Kill the padding oracle by lumping everything into - // a generic error. + tlog.Warn.Printf("DecryptName %q: unPad16 error: %v", cipherName, err) return "", syscall.EBADMSG } - // A name can never contain a null byte or "/". Make sure we never return those - // to the kernel, even when we read a corrupted (or fuzzed) filesystem. - if bytes.Contains(bin, []byte{0}) || bytes.Contains(bin, []byte("/")) { - return "", syscall.EBADMSG - } - // The name should never be "." or "..". - if bytes.Equal(bin, []byte(".")) || bytes.Equal(bin, []byte("..")) { + plain := string(bin) + if err := IsValidName(plain); err != nil { + tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) return "", syscall.EBADMSG } - plain := string(bin) return plain, err } @@ -123,12 +114,16 @@ func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error // // This function is exported because in some cases, fusefrontend needs access // to the full (not hashed) name if longname is used. -func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string) { +func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string, err error) { + if err := IsValidName(plainName); err != nil { + tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err) + return "", syscall.EBADMSG + } bin := []byte(plainName) bin = pad16(bin) bin = n.emeCipher.Encrypt(iv, bin) cipherName64 = n.B64.EncodeToString(bin) - return cipherName64 + return cipherName64, nil } // B64EncodeToString returns a Base64-encoded string diff --git a/internal/nametransform/names_test.go b/internal/nametransform/names_test.go index 9d77c2d..b4e98d4 100644 --- a/internal/nametransform/names_test.go +++ b/internal/nametransform/names_test.go @@ -2,6 +2,7 @@ package nametransform import ( "bytes" + "strings" "testing" ) @@ -51,3 +52,26 @@ func TestUnpad16Garbage(t *testing.T) { } } } + +func TestIsValidName(t *testing.T) { + cases := []struct { + in string + want bool + }{ + {"", false}, + {".", false}, + {"..", false}, + {"...", true}, + {"asdasd/asdasd", false}, + {"asdasd\000asdasd", false}, + {"hello", true}, + {strings.Repeat("x", 255), true}, + {strings.Repeat("x", 256), false}, + } + for _, c := range cases { + have := IsValidName(c.in) + if (have == nil) != c.want { + t.Errorf("IsValidName(%q): want %v have %v", c.in, c.want, have) + } + } +} diff --git a/internal/nametransform/valid.go b/internal/nametransform/valid.go new file mode 100644 index 0000000..b991074 --- /dev/null +++ b/internal/nametransform/valid.go @@ -0,0 +1,27 @@ +package nametransform + +import ( + "fmt" + "strings" +) + +// IsValidName checks if `name` is a valid name for a normal file +// (does not contain null bytes or "/" etc...). +func IsValidName(name string) error { + if name == "" { + return fmt.Errorf("empty input") + } + if len(name) > NameMax { + return fmt.Errorf("too long") + } + // A name can never contain a null byte or "/". Make sure we never return those + // to the kernel, even when we read a corrupted (or fuzzed) filesystem. + if strings.Contains(name, "\000") || strings.Contains(name, "/") { + return fmt.Errorf("contains forbidden bytes") + } + // The name should never be "." or "..". + if name == "." || name == ".." { + return fmt.Errorf(". and .. are forbidden names") + } + return nil +} |