Authentication mechanism absence or evasion - OTP - PHP

Authentication mechanism absence or evasion - OTP - PHP

Need

Implementation of a secure and validated One-Time Password (OTP) authentication mechanism

Context

  • Usage of PHP 7.0 for server-side scripting and web development
  • Usage of Request for making HTTP requests
  • Usage of Validator for input validation and data sanitization
  • Usage of User for managing user-related functionality
  • Usage of Hash for data encryption and password hashing

Description

Non compliant code

        public function register(Request $request)
{
    $validator = Validator::make($request->all(), [
        'name' => 'required|string|max:255',
        'email' => 'required|string|email|max:255|unique:users',
        'password' => 'required|string|min:6|confirmed',
        'phone' => 'required|string|min:10|max:15',
        'otp' => 'required|string|max:6',
    ]);

    if ($validator->fails()) {
        return response()->json($validator->errors()->toJson(), 400);
    }

    $user = User::create([
        'name' => $request->get('name'),
        'email' => $request->get('email'),
        'password' => Hash::make($request->get('password')),
        'phone' => $request->get('phone'),
    ]);

    return response()->json(compact('user'),201);
}
        
        

In the above code, a new user is being registered. The user's name, email, password, phone number, and OTP (One Time Password) are all being validated for basic requirements such as being required, having a maximum length, etc.

However, there is a major flaw in the validation process. The OTP, which is supposed to be a 6-digit code sent to the user's phone number, is not being properly validated. The code only checks if the OTP is present and if it is less than or equal to 6 characters long. It does not check if the OTP is exactly 6 digits, nor does it check if the OTP matches the one that was sent to the user's phone number.

This means that an attacker could potentially bypass the OTP validation by providing any string that is less than or equal to 6 characters long, even if it does not match the OTP that was sent to the user's phone number. This could allow the attacker to register a new account with someone else's phone number, potentially leading to identity theft or other forms of fraud.

Steps

  • Implement a mechanism to generate a unique OTP for each user during registration.
  • Store the generated OTP in a secure manner, such as in a database, along with a timestamp of when it was created.
  • During the registration process, after the user enters the OTP, retrieve the stored OTP for that user from the database.
  • Compare the entered OTP with the stored OTP. If they match, continue with the registration process. If they do not match, return an error message to the user.
  • Implement a time limit for the OTP. If the OTP is not used within a certain time frame (e.g., 10 minutes), it should be considered invalid.
  • After a successful registration, or after the OTP has expired, remove the OTP from the database.

Compliant code

        public function register(Request $request)
{
    $validator = Validator::make($request->all(), [
        'name' => 'required|string|max:255',
        'email' => 'required|string|email|max:255|unique:users',
        'password' => 'required|string|min:6|confirmed',
        'phone' => 'required|string|min:10|max:15',
        'otp' => 'required|string|max:6',
    ]);

    if ($validator->fails()) {
        return response()->json($validator->errors()->toJson(), 400);
    }

    // Generate OTP and store it in the database with a timestamp
    $otp = rand(100000, 999999);
    $otp_expiry = now()->addMinutes(10);
    $user = User::create([
        'name' => $request->get('name'),
        'email' => $request->get('email'),
        'password' => Hash::make($request->get('password')),
        'phone' => $request->get('phone'),
        'otp' => $otp,
        'otp_expiry' => $otp_expiry,
    ]);

    // Send OTP to user's phone number

    // Check if entered OTP matches the stored OTP and is not expired
    if ($request->get('otp') != $user->otp || now() > $user->otp_expiry) {
        return response()->json(['error' => 'Invalid or expired OTP'], 400);
    }

    // Remove OTP from database after successful registration
    $user->otp = null;
    $user->otp_expiry = null;
    $user->save();

    return response()->json(compact('user'),201);
}
        
        

The updated code now includes a mechanism to generate a unique OTP for each user during registration. This OTP is stored in the database along with a timestamp indicating when it was created.

During the registration process, after the user enters the OTP, the stored OTP for that user is retrieved from the database. The entered OTP is then compared with the stored OTP. If they match and the OTP has not expired (it is valid for 10 minutes), the registration process continues. If they do not match or the OTP has expired, an error message is returned to the user.

After a successful registration, or after the OTP has expired, the OTP and its expiry timestamp are removed from the database. This ensures that the OTP cannot be used again, providing an additional layer of security.

References