I have a handler to initiate a password reset. It always returns a successful 200 status code, so that an attacker cannot use it to find out which email addresses are stored in the database. The problem is, if an email is in the database, it'll take a while for the request to be fulfilled (blocking user lookup and sending the actual email with a reset token). If the user is not in the db, the request returns very quickly, so an attacked would know the email is not there.
How would I go about returning the HTTP response right away while processing the request in the background?
pub async fn forgot_password_handler(
email_from_path: web::Path<String>,
pool: web::Data<Pool>,
redis_client: web::Data<redis::Client>,
) -> HttpResponse {
let conn: &PgConnection = &pool.get().unwrap();
let email_address = &email_from_path.into_inner();
// search for user with email address in users table
match users.filter(email.eq(email_address)).first::<User>(conn) {
Ok(user) => {
// some stuff omitted.. this is what happens:
// create random token for user and store a hash of it in redis (it'll expire after some time)
// send email with password reset link and token (not hashed) to client
// then return with
HttpResponse::Ok().finish(),
}
_ => HttpResponse::Ok().finish(),
}
}
You can use an Actix Arbiter
to schedule an asynchronous task:
use actix::Arbiter;
async fn do_the_database_stuff(
email: String,
pool: web::Data<Pool>,
redis_client: web::Data<redis::Client>)
{
// async database code here
}
pub async fn forgot_password_handler(
email_from_path: web::Path<String>,
pool: web::Data<Pool>,
redis_client: web::Data<redis::Client>,
) -> HttpResponse {
let email = email_from_path.clone();
Arbiter::spawn(async {
do_the_database_stuff(
email,
pool,
redis_client
);
});
HttpResponse::Ok().finish()
}
If your database code is blocking, to prevent hogging the long-lived Actix worker threads, you could instead create a new Arbiter
, with its own thread:
fn do_the_database_stuff(email: String) {
// blocking database code here
}
pub async fn forgot_password_handler(email_from_path: String) -> HttpResponse {
let email = email_from_path.clone();
Arbiter::new().exec_fn(move || {
async move {
do_the_database_stuff(email).await;
};
});
HttpResponse::Ok().finish()
}
This may be a bit more work because Pool
and redis::Client
are unlikely to be safe to share between threads, so you will have to solve that too. That's why I didn't include them in the example code.
It's better to use Arbiter
s than be tempted to spawn a new native thread with std::thread
. If you mix the two, you can end up accidentally including code that messes up the worker. For example using std::thread::sleep
in an async
context would pause unrelated tasks that just happen to be scheduled on the same worker, and may not even have any effect on the task you intended.
Finally, you might also consider an architectural change. If you factor database-heavy tasks into their own microservices, you would solve this problem automatically. The web handler can then just send a message (Kafka, RabbitMQ, ZMQ, HTTP, or whatever you choose) and immediately return. This will let you scale the microservices independently of the webserver - 10x web server instances doesn't have to mean 10x database connections, if you only need one instance for the password reset service.