Grindr’s Reset Token Vulnerability: A Technical Deep Dive | Hacker Noon

Dating apps hold a treasure trove of information about their users which can make them an enticing target for malicious actors.

On October 3, 2020, researchers (Wassime Bouimadaghene who found the vulnerability, and Troy Hunt who reported it) announced that they had found a security vulnerability in the dating app Grindr.

This vulnerability allowed anyone to access the password reset link for an account if they knew the user’s email. The password reset page would include the password reset token in its response to the client, this reset token should only be emailed to the user.

The diagram below depicts how this transaction hypothetically should take place.

When the email address is sent as a POST to the server in an attempt to reset the password the server is responsible for a few tasks. The server will determine if the user has an account and then generates a one-time use secure link with a reset token to be emailed to the user.

In this security vulnerability, the server’s response included in the body the reset token needed to access the password reset page. With the combination of the reset token and knowing the pattern that Grindr uses to generate their reset links, any user could perform an account take over.

The complexity of this attack is low, and anyone who can access the development tools for their favorite web browser to take advantage of this. 

Recreating the issue

Although leaking a reset token to the user is a relatively simple error that is not difficult to understand, I wanted to see if I could recreate a working model of the issue and a solution for it. I began by starting up an express server and decided to use nedb for a lightweight database.

The next step in recreating this was to build basic signup, and password reset pages. The sign-up page inserts the user in the database in the following format.

{"name":"TestUser","email":"[email protected]","password":"68eb6fff450606ed183dd8d96c81580f3fb4f35e448cc80c7fwq109e7cec7j076a41f2eww7361c5474e288873f6716e691cgfwcde24d46622d2d482a042ea20","salt":"50d84fpe2r783ea31","createdTime":1603324561115,"_id":"D0sxFz4Z5XnbeewC"}

The format isn’t as important as some of the data I’m storing to use later for generating the reset token. The password hash, creation time, and _id are all used to make the reset token and will allow it to be single-use.

Server-Side

app.post('/forgotPass', function (req, res) {
        
    if (req.body.email !== undefined) {

        var emailAddress = req.body.email;
        //search for user in the database using their email
        db.findOne({ email:emailAddress }, function(err, doc) {         
                if(!doc){
                        //user not found 
                        return  res.send('Email address not in our system');

                }else{
                        //create the secret for the user
                        var secret = doc.password + '-' +doc.createdTime;
                        
                        //build the payload
                        var payload = {
                        id: doc._id, // User ID from database
                        email: doc.email
                        };

                //encode the token using the secret
                var token = jwt.encode(payload, secret);

                //!!reset token leaked to the page!
                        res.json({
                            resettoken: token,
                            status: 'Success'
                        });
                        res.end();      
                        }
                });     

       //send reset link to the user
                transporter.sendMail(mailOptions, function(error, info){
                  if (error) {
                    console.log(error);
                  } else {
                    console.log('Email sent: ' + info.response);
                  }
                });


    } else {
        res.send('User not found');
    }
});

The password reset page is where the security vulnerability in Grindr took place so this is where I will replicate the same issue. To begin I verified that the email address submitted client-side exists in the database, if the user doesn’t exist then I send the message, ‘User not found’.

If the user does exist then I create a secret based on their password hash and the time the user’s password was last generated. The secret is used to encrypt and decrypt the token, it needs to be unique for each user and also unique each time the same user resets their password. Using the hash and the creation time accomplishes this goal.

The last part needed for the JWT is the payload, using the user’s id, and their email address this information can be decrypted later from the token and used to verify the user’s identity. The token is created by using both the payload and the secret and then can later be decrypted server-side by generating the secret again.

Once created the JWT looks like this the following, if you’re not familiar with JWT I’d recommend checking this article out.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IkQwc3hGejRhTWRzYWZYbjEyM3dDIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIn0.DGsr7Nbik7maZ2poyiXsNYR7v2mf1BzyVn-C7kUJgh0

The Token Leak

Normally after the email address is submitted to the server all of the processing would take place and then the server would respond with some information and tell the client whether the reset was successful or not. If successful the user will get a link to reset their password via email. This link contains a reset token appended to the reset URL.

In this case similar to the Grindr reset token leak, I responded back to the client directly in the response body with the reset token along with emailing the user the link to reset. Opening up the development tools you can easily see where the token is being leaked.

If a malicious actor had both the reset token and knew of a user’s email address you can see how they could combine the two pieces of information and access the reset page. This allows any user to reset another users’ account password without needing access to their email account. 

Reset Page Security

app.get('/resetpassword/:email/:token', function(req, res) {
        //find user in the database
    db.findOne({email:req.params.email }, function(err, doc) {  
                
        if(!doc){
         //user not found 
           return  res.send('Go away');
                 
          }else{
           // recreate the secret
                var secret = doc.password + '-' +doc.createdTime;
            //use the secret to decode the token 
                var payload = jwt.decode(req.params.token, secret);

                if(payload.id == doc._id ){
                //send the user the reset form
                   res.send('<form action="/resetpassword" method="POST">' +
                    '<input type="hidden" name="id" value="' + payload.id + '" />' +
                    '<input type="hidden" name="token" value="' + req.params.token + '" />' +
                     '<input type="password" name="password" value="" placeholder="Enter your new password..." />' +
                      '<input type="submit" value="Reset Password" />' +
                                 '</form>');
                   }else{

                     return  res.send('Go away');
                        }

                                
                        }
                });     
                

});

What makes the reset page secure is primarily the JWT. There is not an option to verify the user other than by validating the reset token. This is why it’s critical to protect the reset token as it becomes the validation for a user.

The link pattern I used for the reset link is www.example.com/resetpassword/:email/:token which is easily reconstructed by a malicious actor with the knowledge of an email address and the reset token.

To validate the user I find the email in my database and begin to validate this with the token information. Then, recreate the secret using the same method previously and decode the token with the secret to get the payload.

Once I have the payload I can use the id stored in it to compare against the user’s id stored in the database. If these two id’s match this indicates that the user is valid and that the token has not been tampered with.

Once the users’ identity is verified a simple reset password form is sent to the client that has additional validation by using the reset token.

Conclusion/Solution

     res.json({
                   status: 'Success',
                   Data: any other data here     
                  });
                res.end();

The easiest way to fix this issue is to remove the reset token from the response in the reset page response body, while still ensuring that the client-side browser gets the confirmation needed for the reset request.

This seems simple with such a small example but the more complex the system becomes the harder it is to catch these mistakes.

Grindr luckily fixed the error in a timely fashion and don’t believe that anyone exploited this vulnerability. They are also starting up a new bug bounty program to help prevent these kinds of mistakes from existing in the wild for long periods of time.

References

Tags

read original article here