Account lockout - Go

Account lockout - Go

Need

Prevention of unauthorized account lockouts

Context

  • Usage of Go 1.16 for building scalable and efficient applications
  • Usage of Gin for building web applications in Go
  • Usage of net/http for creating HTTP servers in a Node.js application

Description

Non compliant code

        package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

var loginAttempts = make(map[string]int)

func main() {
	r := gin.Default()

	r.POST("/login", func(c *gin.Context) {
		username := c.PostForm("username")
		password := c.PostForm("password")

		if loginAttempts[username] >= 5 {
			c.JSON(http.StatusForbidden, gin.H{"error": "This account has been locked."})
			return
		}

		// Assume checkLogin is a function that checks if the username and password are correct
		if checkLogin(username, password) {
			c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
		} else {
			loginAttempts[username]++
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
		}
	})

	r.Run()
}

func checkLogin(username, password string) bool {
	// This is just a placeholder. In a real app, you would check the username and password against your database here.
	return username == "admin" && password == "password"
}
        
        

The above Go code is an example of a web server using the Gin framework, which is vulnerable to account lockout. The vulnerability lies in the /login endpoint.

When a POST request is made to the /login endpoint, the server checks if the username and password provided in the form data are correct. If they are correct, a success message is sent back to the client. However, if they are not correct, the server increments a count of failed login attempts for that username.

The problem arises when the count of failed login attempts for a username reaches 5. At this point, the server will respond to any further login attempts with an error message saying that the account has been locked, regardless of whether the correct password is provided.

This means that an attacker could intentionally make 5 failed login attempts for a user, causing that user's account to be locked and preventing the user from being able to login, even with the correct password. This is known as an account lockout vulnerability.

Steps

  • Implement account lockout mechanism with a configurable maximum number of login attempts.
  • Track the number of failed login attempts for each user.
  • Increment the login attempt count for a user when they enter an invalid username or password.
  • Check the login attempt count before allowing a login attempt.
  • If the login attempt count exceeds the maximum allowed attempts, lock the user's account.
  • Return an appropriate error message when the account is locked.

Compliant code

        package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

const maxLoginAttempts = 5
const lockoutDuration = 10 * time.Minute

type User struct {
	Username string
	LoginAttempts int
	LastFailedLogin time.Time
}

var users = make(map[string]*User)

func main() {
	r := gin.Default()

	r.POST("/login", func(c *gin.Context) {
		username := c.PostForm("username")
		password := c.PostForm("password")

		user, exists := users[username]
		if !exists {
			user = &User{Username: username}
			users[username] = user
		}

		if user.LoginAttempts >= maxLoginAttempts && time.Now().Sub(user.LastFailedLogin) < lockoutDuration {
			c.JSON(http.StatusForbidden, gin.H{"error": "This account has been locked."})
			return
		}

		// Assume checkLogin is a function that checks if the username and password are correct
		if checkLogin(username, password) {
			user.LoginAttempts = 0
			c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
		} else {
			user.LoginAttempts++
			user.LastFailedLogin = time.Now()
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
		}
	})

	r.Run()
}

func checkLogin(username, password string) bool {
	// This is just a placeholder. In a real app, you would check the username and password against your database here.
	return username == "admin" && password == "password"
}
        
        

The updated code introduces a lockout mechanism to prevent account lockouts due to excessive login attempts.

A new User struct is introduced to keep track of each user's login attempts and the time of their last failed login. The users map is updated to store pointers to User structs instead of just the login attempt count.

In the login handler, we first check if the user exists in the users map. If not, we create a new User struct for them and add it to the map.

Before checking the login credentials, we now also check if the user has exceeded the maximum number of login attempts ( maxLoginAttempts) and if the lockout duration ( lockoutDuration) has not yet passed since their last failed login. If both conditions are met, we return a "This account has been locked" error and do not proceed with the login attempt.

If the login credentials are correct, we reset the user's login attempt count to 0. If the credentials are incorrect, we increment the login attempt count and update the time of the last failed login.

This way, users are only locked out of their account if they exceed the maximum number of login attempts within the lockout duration. After the lockout duration has passed, they can attempt to login again.

References