sftp: reject an empty password (#11095)

ValidatePassword compared the stored and supplied passwords with
subtle.ConstantTimeCompare, which returns 1 for two zero-length slices.
A user provisioned for public-key-only auth has an empty stored password,
so an empty supplied password authenticated as that user whenever
"password" was among the enabled auth methods (the default).

Treat an empty stored or supplied password as a non-match.

Claude-Session: https://claude.ai/code/session_011QqNaxZwnpHgMoAZNp3RkY
This commit is contained in:
Chris Lu
2026-09-02 11:33:25 -07:00
committed by GitHub
parent 398277a15d
commit 3cdfe648eb
2 changed files with 41 additions and 0 deletions
+5
View File
@@ -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
}
+36
View File
@@ -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)
}
}
}