In that dimly lit hospital room, I lifted that small and fragile baby. Even though my wife just gave birth to him, I instinctively loved him. I vowed to protect him with my life.
There came a time afterwards when we needed a babysitter, so we started the recruitment process.
We would first meet with her to interview her. If we noticed something wrong, we would reject her.
We would then call the parents she used to babysit for. If we heard anything bad about her, we would reject her.
Then comes a trial period where we leave our kid with her and watch her over the nanny cam. If we notice any neglect, we would fire her.
You might think we were paranoid.
But, this is our precious kid. We don’t want any harm to get to him.
Developers and their projects are the same. They created an application from scratch, and saw it grow feature after feature.
Some even call their projects “my baby”.
So, they should protect their babies from the harm that hackers can bring. They should be suspicious of everyone who interacts with their baby and perform all the checks needed to assure his safety.
If you take anything from this post, it is that user input is evil, and you should be suspicious of it. Secure coding requires you to be paranoid.
When receiving user input, consider it harmful.
Before executing a request, perform all the checks necessary to make sure it is legitimate.
You only relax and stop suspecting the input when you check for all the deviations.
Secure coding with the SAFER mental framework
We developed a mental framework to help our clients secure their applications against logical vulnerabilities. We call it the SAFER mental framework.
It is a way of thinking that we encourage developers to adopt.
Why is the mental framework called SAFER? I’m glad you asked. The SAFER framework gets its name from the steps involved in securing an application:
- Speculate on how things should be: brainstorm all the conditions that need to be satisfied in order for the request to pass through. Does the user need to be logged in? Does he need a specific privilege? Does the ID he provided need to be a valid number? Does he need to be the owner of the object he is requesting?
- For each of the conditions you enumerated:
- Assess the absence of the conditions you identified. This takes the form of an if statement. Example: if (! user.loggedIn)
- Fail if the condition is not satisfied. Abort the request. Example: if (! user.loggedIn) return ‘error’;
- Execute: once you assessed all the conditions, it should be safe to execute the user’s request
- Repeat for the other API endpoints.
Let’s dive into each step to better understand the process with concrete examples.
Speculate
This is the most challenging step of the process. This is the creative part. The rest is just building on top of it.
Look at your API code, identify all the user input that you process. Then, ask yourself “What am I expecting the request to look like? And what form is the user input supposed to have?”.
The goal is to enumerate every possible detail about the request. You can do this exercise with other members of your team to cover more ground. And don’t discard anything under the pretext that it’s a small detail.
Let’s clarify all this with an example.
Suppose you want to create a user registration API. The user provides an email address, a password, and a repeat password. The goal is to create a new user in the database. Let’s enumerate the conditions:
- The user calling the API should not be authenticated on the application
- All parameters must be present: email address, password and repeat password
- The email address must be converted to lowercase to avoid conflicts
- The email address the user provided must have the correct format
- The email address must not be already present in the database
- The password and the repeat password must be identical
- The password must respect the password policy
This is the brainstorm phase. No detail is insignificant for this step.
The more tightly you define your expectations, the less maneuvering space the attacker gets.
It doesn’t seem like much, but this is the most important part of the process.
It’s an unusual exercise at first. But, with time and practice you will get better at it.
You will especially improve a lot if you keep learning about what hackers are doing and how they bypass security measures.
For example, I read a while back a blog post about a hacker who confused an application by using lowercase and uppercase letters in his email address.
In that context, he was able to take control over any account on the application.
After reading that, I updated my code to always convert email addresses to lowercase letters.
Secure coding is a continuing process. So, stay alert on the latest hackers’ tricks. One easy way to do that is to follow us on X (Twitter):
Now we continue our implementation of the SAFER mental framework. The rest is easy.
Assess and Fail
I grouped the 2 steps into one because they are linked.
We will check each of the conditions we identified in the previous step.
If the check fails, we abort the operation and the call fails.
Let’s dive into a concrete pseudo-code example:
if ( caller.isAuthenticated() ) return error('you are already authenticated'); if ( nullOrEmpty(email) || nullOrEmpty(password) || nullOrEmpty(password2)) return error('Parameters missing'); email = email.toLower(); if ( ! email.checkEmailFormat() ) return error('Invalid email address'); user_record = getUserByEmail(email); if ( ! nullOrEmpty(user_record)) return error('User already exists'); if ( password != password2) return error('Passwords dont match'); if ( ! checkPasswordPolicy(password)) return error('Password too weak');
Execute
Once you perform all the checks, you are in the clear.
You made sure there are no deviations in the expected input. Everything is as expected, so this is a legitimate request.
You can go ahead and execute the requested action.
Here is the pseudo-code one more time:
if ( caller.isAuthenticated() ) return error('you are already authenticated'); if ( nullOrEmpty(email) || nullOrEmpty(password) || nullOrEmpty(password2)) return error('Parameters missing'); email = email.toLower(); if ( ! email.checkEmailFormat() ) return error('Invalid email address'); user_record = getUserByEmail(email); if ( ! nullOrEmpty(user_record)) return error('User already exists'); if ( password != password2) return error('Passwords dont match'); if ( ! checkPasswordPolicy(password)) return error('Password too weak'); registerUser(email, password);
If you want to see an implementation in your favorite programming language, check our user account management example.
Repeat
Now that you secured one endpoint, you need to do it again for all other endpoints.
Make sure you don’t forget any endpoint.
And take your time in the Assess step to identify as many conditions as possible.
Security by iteration
This framework isn’t something that you execute once and forget about.
You need to implement it every time you add a feature to an existing endpoint and every time you create a new endpoint.
Also, when you immerse yourself in the process, you will get ideas of conditions to add to the Assess phase at random moments.
Maybe while running, or while showering, or before going to bed.
Make sure to note those conditions somewhere and to implement the check when you are in front of your computer.
Other times, you read a hacker’s write-up and realize you might be vulnerable to the same attack. In that case also don’t waste time, and implement the check quickly.
Is this all I need to secure an application?
Unfortunately no, secure coding requires you to do more. There are 2 types of vulnerabilities you need to protect yourself against. Logical vulnerabilities and technical vulnerabilities.
The SAFER mental framework helps with the logical vulnerabilities. When you implement it correctly, your application will be secure against business logic bypasses and other vulnerabilities that try to force the application to do something it wasn’t supposed to.
For technical vulnerabilities there is no shortcut. You need to know about all the vulnerabilities (SQL injection, XSS, XXE, LDAP injection, code injection …) and make sure you are protected against them.
But, if you do both, your application will be secure. That’s a guarantee.
Leave a Reply