I am trying to implement email confirmation on my project. I sign a JWT which is sent in an email using nodemailer. After the link is clicked the JWT is sent to the server for processing. I am able to find out if the JWT matches the one generated by the server. However I am now struggling with updating the database to store that the user has confirmed the email.
Controller
confirmEmail = async (req, res, next) => {
this.checkValidation(req)
const { email } = req.body
const param = req.params.token
const user = await userModel.findOne({ email })
if(!user)
{
throw new HttpException(401, 'User not found')
}
if(user.confired)
{
throw new HttpException(401, 'User already confirmed')
}
if(!user.confirmed)
{
const confirmJWT = jwt.verify(param, process.env.SECRET_JWT)
if(!confirmJWT)
{
throw new HttpException(200, 'Token failed')
}
const result = await userModel.emailConfirmed(email)
throw new HttpException(401, 'User not cofirmed')
}
}
Model
emailConfirmed = async({email}) => {
const sql = `UPDATE ${this.tableName} SET confirmed = true WHERE email = ?`
const result = await query(sql, email)
return result
}
[Error] Error: Incorrect arguments to mysqld_stmt_execute
this is the error I get when attempting to change the db.
Related
I have this forgot password handler in my Express app. I send an email with a crypto.randomBytes(20).toString("hex") token, that I use later to verify the request. It is working perfectly.
However, I have seen that people are hashing this token before sending it and storing in the data base, and I don't see why, cause it is already a random string.
const forgotPassword = async (req, res) => {
try {
const user = await User.findOne({ email: req.body.email });
if (!user) {
throw Error("incorrect email");
}
const resetPasswordToken = crypto.randomBytes(20).toString("hex");
user.resetPasswordToken = resetPasswordToken;
user.resetPasswordTokenExpire = Date.now() + 10 * (60 * 1000);
await user.save();
const message = `
<h1>You have requested a password reset</h1>
<p>Here is your token : </p>
<p>${resetPasswordToken}</p>
`;
try {
await sendEmail({
to: user.email,
subject: "Password reset request",
text: message,
});
res.status(200).json({ message: "Email sent" });
} catch (err) {
user.resetPasswordToken = undefined;
user.resetPasswordTokenExpire = undefined;
res.status(500).json({ message: "Email could not be sent" });
}
} catch (error) {
console.log(error);
const errorDetails = handleErrors(error);
res.status(400).json(errorDetails);
}
};
If you hash the token and only save the hash into the database, you can make sure that admins and other people who are able to access the database cannot use the token to reset a password for a different user.
It's basically the same reason why you hash (and salt and pepper) passwords, because you don't want that the original string can be recreated whenever somebody has access to the table.
I'm trying to beat this CTF: a website under construction that has a simple login page where you can login or register a new user.
It uses node express and a SQLite DB.
Analyzing the source code I found this query:
getUser(username){
return new Promise((res, rej) => {
db.get(`SELECT * FROM users WHERE username = '${username}'`, (err, data) => {
if (err) return rej(err);
res(data);
});
});
},
This function gets called after a Middleware checked session cookies with jsonwebtoken to retrieve the username when you GET '/'.
This is where it's called:
router.get('/', AuthMiddleware, async (req, res, next) => {
try{
let user = await DBHelper.getUser(req.data.username);
if (user === undefined) {
return res.send(`user ${req.data.username} doesn't exist in our database.`);
}
return res.render('index.html', { user });
}catch (err){
return next(err);
}
});
AuthMiddleware:
module.exports = async (req, res, next) => {
try{
if (req.cookies.session === undefined) return res.redirect('/auth');
let data = await JWTHelper.decode(req.cookies.session);
req.data = {
username: data.username
}
next();
} catch(e) {
console.log(e);
return res.status(500).send('Internal server error');
}
}
Since that appears to be the only query formatted that way (the others all use ?) and in general the only evident vulnerability, I suspect the flag is stored somewhere in the database.
As the only way to get that function called is to have an active session i registered a user with 'malicious' sql code as the username. At first I tried to close the quote and attach an OR WHERE to get all the users:
SELECT * FROM users WHERE username = '${username}' +
bob' OR WHERE username IS NOT NULL =
SELECT * FROM users WHERE username = 'bob' OR WHERE username IS NOT NULL
This should at least throw an error as WHERE username = 'bob' OR WHERE username IS NOT NULL should return a collection with all the users in the database while it's rendered on the webpage as
Welcome {{ user.username }}
This site is under development.
Please come back later.
I was expecting at least "no username property on array" or something like that. Instead it always return the full username I gave him
Welcome bob' OR WHERE username IS NOT NULL
This site is under development.
Please come back later.
Am I missing something? Is there a way to escape eventual quotes that might be added during the cookie reading?
EDIT:
Here is the function that gets called when you attempt a login
/auth route:
router.post('/auth', async (req, res) => {
const { username, password } = req.body;
if((username !== undefined && username.trim().length === 0)
|| (password !== undefined && password.trim().length === 0)){
return res.redirect('/auth');
}
if(req.body.register !== undefined){
let canRegister = await DBHelper.checkUser(username);
if(!canRegister){
return res.redirect('/auth?error=Username already exists');
}
DBHelper.createUser(username, password);
return res.redirect('/auth?error=Registered successfully&type=success');
}
// login user
let canLogin = await DBHelper.attemptLogin(username, password);
if(!canLogin){
return res.redirect('/auth?error=Invalid username or password');
}
let token = await JWTHelper.sign({ // Maybe something can be done with this function?
username: username.replace(/'/g, "\'\'").replace(/"/g, "\"\"")
})
res.cookie('session', token, { maxAge: 900000 });
return res.redirect('/');
});
attemptLogin():
attemptLogin(username, password){
return new Promise((res, rej) => {
db.get(`SELECT * FROM users WHERE username = ? AND password = ?`, username, password, (err, data) => {
if (err) return rej();
res(data !== undefined);
});
});
}
EDIT 2.0:
I just noticed the part where it stores the session cookie:
let token = await JWTHelper.sign({
username: username.replace(/'/g, "\'\'").replace(/"/g, "\"\"")
})
It apparently replaces all ' with \'\'. I can solve half of that by escaping the quote so that it becomes \\'\'. This allows me to close the username='' statement, but I still need to find a way to invalidate the second \'.
The purpose of the following code is to check whether an email already exists in MongoDB, using express-validator:
app.post('/registerPage',[check('email').custom((email) => {
// connect to database
let MongoClient = require('mongodb').MongoClient;
let url = 'mongodb://localhost';
MongoClient.connect(url, function(err, client) {
if (err) throw err;
let db = client.db('Mydatabase');
// search database
return db.collection('users').findOne({
email: email
}).then(user => {
if (user) {
console.log(user); // here console shows correct record in database
return Promise.reject('E-mail already in use');
}
// otherwise, it returns null
});
})
}).withMessage('Error Message Example')], (req, res) => {
// Handle the request
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() })
}
});
When email already exists, console shows Promise.reject('E-mail already in use');.
The problem is when email does not exist, although it doesn't show Promise.reject, the code cannot process any further, validationResult(req) is not empty, so it still prints out the error message 'Error Message Example'. But there isn't any problem with non-custom validators which can successfully pass the checks.
I tried to add an else statement where !user, it doesn't work.
The question is how to pass the custom validation check, or why the array validationResult(req) is not empty even it should be? How do I make sure validationResult is empty after all checks were passed.
The issue is you are returning the promise in the callback of MongoClient.connect and not the validator function. Try using Promise wrapper like:
app.post('/registerPage',[check('email').custom((email) => {
return new Promise((resolve, reject) => {
// connect to database
let MongoClient = require('mongodb').MongoClient;
let url = 'mongodb://localhost';
MongoClient.connect(url, function(err, client) {
if (err) throw err;
let db = client.db('Mydatabase');
// search database
return db.collection('users').findOne({
email: email
}).then(user => {
if (user) {
console.log(user); // here console shows correct record in database
return reject('E-mail already in use');
}
return resolve();
});
})
});
}).withMessage('Error Message Example')], (req, res) => {
// Handle the request
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() })
}
});
Hope this helps!
I'm trying to create a data entry on the firebase database to store additional information about a user when they register on my site.
I've tried to write data to the database in the .then() function following createUserWithEmailAndPassword() as that's the only way for me to extract the user id for the user (I'm hoping to use the uid as the key field of the record I create)
(req, res) => {
// extract user data from the form
const newUser = {
fname: req.body.fname,
lname: req.body.lname,
email: req.body.email,
pw: req.body.pw,
pw_c: req.body.pw_c
}
// carry out validation
const { valid, errors } = validateRegistrationData(newUser);
if (!valid) return res.status(400).json(errors);
// create new firebase user
firebase
.auth()
.createUserWithEmailAndPassword(newUser.email, newUser.pw)
.then(data => {
let uid = data.user.uid;
// make a database entry to store the users info
// by default, assumes that the user is a secondary user
let userData = {
fname: newUser.fname,
lname: newUser.lname,
email: newUser.email,
utype: 1,
createdon: admin.firestore.FieldValue.serverTimestamp(),
intitems: []
}
newUserDoc = db
.collection("users")
.doc(uid)
.set(userData)
return res.status(200).json("Success: new user created.");
})
.catch(err => {
if (err.code === "auth/email-already-in-use"){
return res.status(400).json({ email: "Email is already in use" });
} else {
return res.status(500).json({ error: err.code });
}
});
return res.status(200).json("Success: new user created.");
}
The server responds with {Success: new user created."}. The authentication part seems to work as a new user is created in the Authentication section of my firebase console. However, no new data entries appear in the users collection of my database.
.set returns a promise that still needs to run to completion. However, currently you're not waiting on the promise, and instead just responding via res.send.
You can append .then(() => { do stuff here }) to the end of .set. If it's the last thing you're doing in that function, you can just do res.send from there.
return db.collection("users").doc(uid).set(userData).then(() => {
return res.status(200).json("Success: new user created.");
})
.catch(error => {
console.log(error)
})
I have a login in my Angular 2 app, and I have been converting it from using a fake backend (which works) to connect to our mongoDB-based API instead.
This is the login function I am using in the authentication service:
login(username: string, password: string) {
const u = encodeURIComponent(username);
const p = encodeURIComponent(password);
this._url = `https://api.somesite.com/v0/staff/login/${u}/${p}?apikey=somekey`;
console.log(this._url);
return this.http.post(this._url, JSON.stringify({ username: username, password: password }))
.map((response: Response) => {
// login successful if there's a jwt token in the response
const user = response.json();
if (user && user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify(user));
}
});
}
In my login component I am subscribing like this:
login() {
this.loading = true;
this.authenticationService.login(this.model.username, this.model.password)
.subscribe(
data => {
this.router.navigate(['/']);
console.log('User logged in as: ' + this.model.username);
},
error => {
this.alertService.error(error);
this.loading = false;
});
this.authenticationService.username = this.model.username;
}
When I try this, and log to the console "this_url", I get what I would expect. For instance, if the user typed in "billsmith" for username, and "parisnow" for password, I see this in the console for "this_url":
https://api.somesite.com/v0/staff/login/billsmith/parisnow?apikey=somekey
Furthermore, I can type that url directly into the browser address window and see data (when the username and password correctly correspond to actual records in our database). So it's accessing the correct info in that sense.
But in the console I get a "404" error for that generated url. It also doesn't "do anything". In other words, it doesn't correctly redirect to the main component as it did with the fakeBackend-enabled login. And the only thing that's different now is the url that I am calling (because I'm connecting to our actual API now, as opposed to a fake backend provider).
FYI, the url when using the fake backend looked like this:
return this.http.post('/api/authenticate', JSON.stringify({ username: username, password: password}))
What am I missing here?
By the way, this is how things look on the server side re: our mongoDB:
exports.byLogin = function(req, res, next) {
let ioOnly = false, username, password;
if (_.isUndefined(req.params)){
ioOnly=true;
username = req.username;
password = req.password;
}
else {
username = req.params.username;
password = req.params.password;
}
staff.findOne({username: username, password: password}, function(err, doc) {
if (err) { if (!ioOnly) { return next(err) } else { return res(err)}}
else if(doc) ((!ioOnly) ? res.send(doc) : res(doc));
else ((!ioOnly) ? res.sendStatus(204) : res(doc));
});
};