atomic-toctou-race-registration.md 3.3 KB

TOCTOU race condition при регистрации пользователя

Контекст: В backend/internal/services/auth.go:85-86, метод Register сначала проверяет существование email, а потом создаёт пользователя. Между этими двумя операциями возможна гонка.

Проблема

func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, string, error) {
    // Шаг 1: Check (Time-of-Check)
    existing, _ := s.userRepo.GetByEmail(ctx, input.Email)  // ❌ ошибка подавлена
    if existing != nil {
        return nil, "", ErrEmailExists
    }

    // ⚡ В этот момент другой запрос может создать пользователя с тем же email

    // Шаг 2: Act (Time-of-Use)
    if err := s.userRepo.Create(ctx, user); err != nil {
        return nil, "", fmt.Errorf("create user: %w", err)
    }
}

Две проблемы:

  1. Подавление ошибки: _ игнорирует ошибку GetByEmail. Если БД недоступна — пользователь получит "email already exists" вместо ошибки сервера.
  2. TOCTOU race: два одновременных запроса с одинаковым email могут пройти проверку и оба создать пользователя.

Решение

Использовать уникальный constraint на уровне БД + проверять ошибку Create:

-- миграция (уже есть)
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, string, error) {
    // ... хеширование пароля, создание User ...

    if err := s.userRepo.Create(ctx, user); err != nil {
        // Проверяем, что ошибка — нарушение уникального constraint
        if isDuplicateEmailError(err) {
            return nil, "", ErrEmailExists
        }
        return nil, "", fmt.Errorf("create user: %w", err)
    }

    return s.generateTokens(ctx, user)
}
// repository/users.go — проверка ошибки pgx
func isDuplicateEmailError(err error) bool {
    var pgErr *pgconn.PgError
    return errors.As(err, &pgErr) && pgErr.Code == "23505" // unique_violation
}

Альтернативы

Подход Плюсы Минусы
Unique constraint + проверка ошибки Атомарно, стандартная практика Зависит от кода ошибки pgx
Транзакция + FOR UPDATE Можно сделать доп. проверки Блокировка строки, медленнее
Optimistic locking Нет блокировок Больше кода

Связанные заметки

  • [[backend-auth-security]] — контекст auth сервиса
  • [[architecture-overview]] — таблица P0

Источник

Code review PhotoPlaces 2026-06. services/auth.go:85-86.

Теги

#concurrency #database #security #golang #race-condition #best-practice