Password reset poisoning - Dart

Password reset poisoning - Dart

Need

Secure password reset functionality

Context

  • Usage of Dart for building cross-platform mobile, web, and desktop applications
  • Usage of Shelf for managing data storage and retrieval
  • Usage of shelf_router for creating a router middleware in Shelf framework

Description

Non compliant code

        import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_router/shelf_router.dart';

class UserController {
  Router _router;

  UserController() {
    _router = Router();
    _router.post('/reset-password', (shelf.Request request) async {
      final params = await request.readAsString();
      final email = params['email'];

      // Generate reset link
      final resetLink = 'http://yourapp.com/reset?token=123456&email=$email';

      // Send the reset link to the user's email
      // This is just a placeholder. In a real world scenario, you would use an email service.
      print('Password reset link: $resetLink');

      return shelf.Response.ok('Password reset link sent.');
    });
  }

  shelf.Handler get handler => _router;
}
        
        

In the above Dart code, we have a UserController class that handles user related operations. It uses the shelf_router package to define routes for the application.

The /reset-password POST endpoint is used to generate a password reset link for the user. It reads the email from the request, generates a reset link, and sends it to the user's email.

The vulnerability lies in the generation of the reset link. The domain of the reset link is hard-coded as http://yourapp.com. An attacker can manipulate the request and change the domain to one under his control.

When the reset link is clicked, the request goes to the attacker's server with the reset token and the user's email. This allows the attacker to reset the user's password, effectively gaining control over the user's account.

This is a classic example of Password Reset Poisoning where the application does not properly validate the reset link's domain.

Steps

  • Use HTTPS instead of HTTP for the reset link to ensure the communication is secure.
  • Do not include the email in the reset link. Instead, associate the token with the user's email on the server side.
  • Generate a unique, random token for each password reset request. Do not use a static token.
  • Ensure the token expires after a certain period of time or after it's used.
  • Validate the token server-side before allowing the password reset.

Compliant code

        import 'dart:math';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_router/shelf_router.dart';

class UserController {
  Router _router;
  Map tokens = {};

  UserController() {
    _router = Router();
    _router.post('/reset-password', (shelf.Request request) async {
      final params = await request.readAsString();
      final email = params['email'];

      // Generate a unique, random token for each password reset request
      final token = _generateToken();

      // Associate the token with the user's email on the server side
      tokens[token] = email;

      // Generate reset link
      final resetLink = 'https://yourapp.com/reset?token=$token';

      // Send the reset link to the user's email
      // This is just a placeholder. In a real world scenario, you would use an email service.
      print('Password reset link: $resetLink');

      return shelf.Response.ok('Password reset link sent.');
    });
  }

  String _generateToken() {
    var rng = Random();
    var codeUnits = List.generate(20, (index){
      return rng.nextInt(33)+89;
    });

    return String.fromCharCodes(codeUnits);
  }

  shelf.Handler get handler => _router;
}
        
        

The updated code now generates a unique, random token for each password reset request. This token is associated with the user's email on the server side, and is included in the password reset link. The link now uses HTTPS to ensure secure communication.

The _generateToken method generates a random 20-character string to be used as the token. This token is stored in a map along with the user's email.

When the user clicks on the reset link, the server should validate the token and check if it's expired before allowing the password reset. This validation is not included in the code above, but should be implemented in the route handling the password reset.

The email is no longer included in the reset link, preventing an attacker from changing the email parameter to one under his control. The token is associated with the user's email on the server side, ensuring that the password reset is performed for the correct user.

The token should be set to expire after a certain period of time or after it's used to further enhance security. This is not included in the code above, but should be implemented in your application.

References