Przeglądaj źródła

security(user): don't leak usernames existence via timing

When a non-existent username was submitted, CheckPassword returned
immediately without performing a bcrypt comparison, making it possible
to distinguish valid from invalid usernames by measuring response time.

Perform a dummy bcrypt comparison against a fixed cost-10 hash when
the user is not found so the response time is indistinguishable from
a real password check.
jvoisin 1 miesiąc temu
rodzic
commit
83ea3d1912
1 zmienionych plików z 4 dodań i 0 usunięć
  1. 4 0
      internal/storage/user.go

+ 4 - 0
internal/storage/user.go

@@ -16,6 +16,8 @@ import (
 	"golang.org/x/crypto/bcrypt"
 )
 
+var dummyBcryptHash = []byte("$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy")
+
 // CountUsers returns the total number of users.
 func (s *Storage) CountUsers() (int, error) {
 	var result int
@@ -673,6 +675,8 @@ func (s *Storage) CheckPassword(username, password string) error {
 
 	err := s.db.QueryRow("SELECT password FROM users WHERE username=$1", username).Scan(&hash)
 	if errors.Is(err, sql.ErrNoRows) {
+		// Perform a dummy bcrypt comparison against the hashed `password` string to avoid leaking whether the user exists via response timing.
+		_ = bcrypt.CompareHashAndPassword(dummyBcryptHash, []byte(password))
 		return fmt.Errorf(`store: unable to find this user: %s`, username)
 	} else if err != nil {
 		return fmt.Errorf(`store: unable to fetch user: %v`, err)