diff --git a/weed/sftpd/user/filestore.go b/weed/sftpd/user/filestore.go index 5bd0b0513..24723fe8d 100644 --- a/weed/sftpd/user/filestore.go +++ b/weed/sftpd/user/filestore.go @@ -154,6 +154,11 @@ func (s *FileStore) ValidatePassword(username string, password []byte) bool { return false } + // An empty stored or supplied password is never a match: a public-key-only + // user has no password, and equal-length zero slices would otherwise compare equal. + if len(user.Password) == 0 || len(password) == 0 { + return false + } // Compare plaintext password using constant time comparison for security return subtle.ConstantTimeCompare([]byte(user.Password), password) == 1 } diff --git a/weed/sftpd/user/filestore_test.go b/weed/sftpd/user/filestore_test.go new file mode 100644 index 000000000..9c097c420 --- /dev/null +++ b/weed/sftpd/user/filestore_test.go @@ -0,0 +1,36 @@ +package user + +import "testing" + +func newTestStore(users ...*User) *FileStore { + s := &FileStore{users: make(map[string]*User)} + for _, u := range users { + s.users[u.Username] = u + } + return s +} + +func TestValidatePasswordRejectsEmpty(t *testing.T) { + s := newTestStore( + &User{Username: "keyonly", Password: "", PublicKeys: []string{"ssh-ed25519 AAAA"}}, + &User{Username: "haspass", Password: "s3cret"}, + ) + + cases := []struct { + username string + password string + want bool + }{ + {"keyonly", "", false}, // public-key-only user must not accept an empty password + {"keyonly", "wrong", false}, + {"haspass", "", false}, // a real password is never matched by an empty one + {"haspass", "s3cret", true}, + {"haspass", "wrong", false}, + {"missing", "", false}, + } + for _, c := range cases { + if got := s.ValidatePassword(c.username, []byte(c.password)); got != c.want { + t.Errorf("ValidatePassword(%q, %q) = %v, want %v", c.username, c.password, got, c.want) + } + } +}