How to protect and process user data in a modern web application?

Why did I write this article?
I have always disliked articles where you are given unrelated advice, overloaded with unnecessary details and implementations.
I believe that articles should cover the topic as accurately as possible, without being overloaded with details, give references, show simple implementations in accordance with best practices.
And most importantly, in my opinion, training and tutorials should be "all in one", so that you don't get nervous about "what if I missed something".
Also, importantly, when people with experience try to teach others, they forget exactly what was difficult for them when they had no experience.
That's why I write my blog - I want to help people by giving more or less comprehensive answers to big topics.
So, I've put together a few topics that I had previously found difficult or incomprehensible regarding dealing with user data and information security in modern web applications. When I did this, I was answering the question, "what kind of guidance would I like to see when I deal with these topics?"
So, you have a PHP application (let's say it's a REST API in Lumen), you want to protect yourself against common attacks, how do you do that? The first thing you need to keep in mind is:
🖐️
If the security of your system is not based on cryptography, but on obfuscation, some secrets or stealth, then your system is vulnerable.
Next, you just need to follow simple guidelines and ready-made solutions to improve your security.
Protecting user passwords
How to store passwords securely in a database? Is it possible to store passwords "as is"? What happens if there is a leak and a table with logins and passwords gets into the hands of hackers?
To keep passwords in a database safe, they need to be hashed. If passwords are hashed, even during a leak, hackers won't be able to use the passwords and log on as other users.
To create a password hash with PHP, use the function:
password_hash("a_strong_password", PASSWORD_DEFAULT);
The generated string can be safely saved to the database.
But what to do when a user enters his password? How do I compare simple string and hash? How can I make sure that the user is entering the correct password? The "password_verify" function exists for this purpose.
To check the password that the user sends when he tries to login, you need to use the following code
<?php
$hashedPasswordInDatabase = password_hash("a_strong_password", PASSWORD_DEFAULT);
if (password_verify("a_strong_password", $hashedPasswordInDatabase)) {
echo "You are logged in";
} else {
echo "Password or login is incorrect";
}
// Obviously, this will print "You are logged in"
What kind of "salt"?
But how do you protect yourself from rainbow tables or dictionary attacks? To protect yourself from these attacks, you need to use salt when generating your password hash. Fortunately for us, the "password_hash" function generates the salt automatically! You don't have to do anything else on your own.
Restoring user passwords
What should I do if the user has forgotten his password? Obviously, you should email him with a link like
/password-recovery?unique_token=4427fadc179d4ec97bda65275904dd5f
Simply put, how to generate a link to recover a password? The string must be cryptographically secured and must have a high enough uniqueness to find the user through this link and allow him to change the password.
To do this, use this code:
$token = bin2hex(random_bytes(16));
Okay, we sent the user a link and he clicked on it. How do we make sure the user is who he says he is and change his password?
You will need to create a table with the token, its expiration time and the user id. Accordingly, after the user has clicked the link and sent a new password to the form, you can find the user in the table by his token and, if it exists, change his password. If the user has entered a token that is not in this table or is expired, he is trying to hack us.
user_id | token | expiration_date
Sanitize input
Okay, it's time to talk about user input processing. The most important rule:
🖐️
Never trust what the user sends. Always treat it as an attack on your system.
This means you have to pre-process the user's input before you start working with it.
To achieve this, use the function "filter_var"
$smaller = "not a tag < 5";
echo filter_var ( $smaller, FILTER_SANITIZE_STRING); // -> not a tag
You can find a list of all filters here
These filters are most often used:
FILTER_SANITIZE_EMAIL
FILTER_SANITIZE_NUMBER_INT
FILTER_SANITIZE_NUMBER_FLOAT
FILTER_SANITIZE_STRING
Use prepared statements
We have already learned that we cannot trust the data sent by the user and are sanitizing the data. But do not rely on this, because we still leave a "window" for SQL injection.
The best way to prevent SQL injection is to use prepared statements. If you are interested in the theory, you can read about it here.
Let's turn to practice. Here is an example from the official documentation
<?php
$mysqli = new mysqli("example.com", "user", "password", "database");
$mysqli->query("CREATE TABLE test(id INT, label TEXT)");
/* Prepared statement, stage 1: prepare */
$stmt = $mysqli->prepare("INSERT INTO test(id, label) VALUES (?, ?)");
/* Prepared statement, stage 2: bind and execute */
$id = 1;
$label = 'PHP';
$stmt->bind_param("is", $id, $label); // "is" means that $id is bound as an integer and $label as a string
$stmt->execute();
"But I only work with modern ORMs"
Modern frameworks and ORMs usually protect you from sending data directly to queries, but it's still worth keeping this vulnerability in mind.
You can say that from now on, your application is protected from numerous threats related to data and its processing. It is important to remember that you should not trust the data that the user sends in. It is also important to remember that you should only rely on cryptographically stable methods for storing data, and not on half-secret methods or methods based on obfuscation.
To receive articles, guides, and tutorials, subscribe to my newsletter (click the button above). See you there!