Security is one of the most important components of your web app. You should keep that in mind throughout the entire development process. It is especially important to ensure the security of fintech projects and services that store corporate information or private user data.
Everyone involved in product development should be aware of possible security issues and the way hackers could exploit them.
In 2017, the Open Web Application Security Project (OWASP), an international non-profit organization whose goal is to improve software security across the globe, presented a list of the top 10 most critical web application security risks. This list is still up-to-date, and programmers still make mistakes that lead to critical security issues.
In this article, we will tell you about the most common security pitfalls, how hackers can take advantage of them, and how to avoid them.
Injection
The most common and hazardous type of vulnerability occurs when untrusted data is sent to an interpreter as part of a command or query. The attacker can trick the interpreter by inserting special commands or accessing sensitive data without proper authorization.
There are many different types of injection flaws, such as SQL, NoSQL, OS, and LDAP injection.
How can this happen?
Let's imagine that the user gets a list of all the bills for one of their real estate objects from one of the URLs. The following SQL query is made to the database:
SELECT * FROM bill WHERE object_id = 5
Here, 5 is a parameter passed by the client and inserted into the query to the server. However, if the attacker passes that same string with 1 OR 1=1
instead, the query will result in:
SELECT * FROM bill WHERE object_id = 1 OR 1=1
This way, the attacker will get all the bills in the database, regardless of whether they have access to it or not.
What are the consequences?
- The attacker is able to obtain access to sensitive data (users’ personal info, passwords, etc.).
- The attacker is able to modify or delete data.
How to prevent it?
You should use popular Object Relational Mapping Tools (ORMs) when working with databases because they will safely parameterize certain types of queries.
If there is a situation when you can't use an ORM, pay attention to user input. It must be validated and escaped before inserting it into database queries.
XSS (Cross-Site Scripting)
This vulnerability allows an attacker to execute scripts in the browser of a user who is viewing a page with malicious code.
How can this happen?
Let's imagine that your site allows you to publish various content — articles, comments, etc., and the attacker sends the following info instead of posting a regular comment:
<script>
console.log('XSS Attacks');
// any JS code that will be executed in the user's browser
</script>
After submitting the form, this code is saved without processing and placed on all pages where the comment should have been displayed.
As a result, this JavaScript code will be executed in the browser of the user viewing the page.
What are the consequences?
An XSS attack can execute any JavaScript code, so only the attacker's imagination can limit its consequences. This means any elements can be added to the page, user cookies (including authorization tokens) can be stolen, and the users may be redirected to unwanted or malicious sites.
How to prevent it?
- Never use the
eval
function in JavaScript. - Any user input must be escaped before being inserted into the DOM and/or saved in the database. In other words, all special symbols must be replaced. For example, the above code will look like this after escaping:
&lt;script&gt;
console.log(&lsquo;XSS Attacks&rsquo;);
// any JS code that will be executed in the user’s browser
&lt;/script&gt;
There are a lot of great npm packages that solve the issue of escaping. Here is one of them.
CSRF Attacks (Cross-Site Request Forgery)
This vulnerability allows for a forged request to be sent to your server from the attacker's website.
How can this happen?
Let's assume that your web application stores the user's session in cookies, which are sent to the server with each request. In turn, the server checks the session from the incoming cookies and decides whether the request is authorized.
<html>
<body>
<form action="https://mysite.com/email/change" method="POST">
<input type="hidden" name="email" value="bad.email@gmail.com" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
After that, the attacker sends the link to this page to the victim’s email.
When the victim clicks on the link, a request will be sent to the server: "https://mysite.com/email/change.” The request will include the victim's session cookie and any other automatically included authentication information. As a result, the request to change the email address will be authorized and executed successfully.
What are the consequences?
The attacker will be able to make unauthorized requests on behalf of the user. Those could be requests to transfer money, publish a message, add an administrator, or change the email address. It all depends on the specific application.
How to prevent it?
You must configure Cross-Origin Resource Sharing (CORS) correctly on the server side. CORS will tell browsers to give a web application running at one origin access to selected resources from a different origin.
A popular method of protection is a unique token sent with each request.
The mechanism is as follows:
- The server generates a unique token for each authorized user.
- This unique token is sent to the client within the HTML meta tag when rendering the page or is added to the response headers in the SPA application.
- On the client side, this token is sent to the server along with a form or request to change data. This way, the server can define whether this action was performed by a real user or by an attacker.
Using Bearer Scheme and other authentication schemes will also protect your web application from CSRF attacks. In this case, an attacker can't fake headers without knowing the authorization token of a specific user.
Broken Access Control
Restrictions on what authenticated users are allowed to do are often not properly enforced, and the users may access unauthorized functionality and/or data.
What are the consequences?
An attacker who finds such a vulnerability can make requests to obtain, edit, or delete sensitive data.
How to prevent it?
- With the exception of public resources, deny access by default.
- Access control should only be performed on the trusted side, i.e., on the server. If access control is carried out on the client side, it will only restrict ordinary users and won't protect you from attacks.
- Implement access control mechanisms once and reuse them throughout the application. This will prevent any mistakes from happening.
- Log access control failures and alert admins when appropriate (e.g., repeated failures).
Insecure Deserialization
This vulnerability occurs when the server deserializes objects from untrusted sources. This problem most often leads to other types of vulnerabilities, such as those related to access control.
How can this happen?
Let's assume that the web app saves a serialized object with data about the current user in cookies and then uses it to control access.
An attacker can change the information stored in a serialized object: for example, switch the standard role to the administrator role. After the server receives this serialized object and tries to deserialize it, it will assume that the attacker has the administrator role and allow the attacker to perform forbidden actions. This example is directly related to broken access control and perfectly describes two security issues at once.
What are the consequences?
By falsifying data in a serialized object, an attacker can modify application logic after deserialization or get permission to perform actions that are not allowed.
How to prevent it?
Accept serialized objects only from trusted sources when you are 100% sure that an attacker cannot modify them.
Broken Authentication and Session Management
This problem is pervasive and is caused not only by technical issues but also by users who are carefree with their passwords and security in general. We must develop a technically secure system that protects users even when unauthorized access is their fault. Therefore, the next three sections are dedicated to user authorization only.
Сookies' Theft (including authorization tokens)
Unfortunately, we cannot protect the user's cookies with a 100% success rate because there may already be malware on the user's device, the user may be using a browser with security issues, or cookies may be copied manually if someone gets access to the user's device.
What are the consequences?
Having obtained the necessary data for the authentication of requests, the attacker gets full access to the victim's account for a period equal to the lifetime of the session or an indefinite period. It all depends on the implemented authentication scheme.
How to prevent it?
First, we must do everything we can to protect the user's cookies. For this to happen, use only the HTTPS protocol and set cookies with the Secure and HttpOnly flags. Secure allows sending cookies to the server only if the request is made over SSL and HTTPS. HttpOnly makes cookies unavailable from JavaScript, which will protect you from theft if there is an XSS vulnerability.
But all this does not guarantee the safety of cookies, so we need to prepare for a situation when an attacker will steal tokens or a session. The unauthorized access has to be detected as soon as possible, and special measures to protect the user's sensitive data need to be taken.
One way is to use a pair of accessToken
and refreshToken
tokens.
accessToken
is a reusable token that is utilized to authenticate users and perform specific actions. You could get access for a short period, from a few dozen minutes to several hours. The token is passed in the header Authorization: Bearer {accessToken}
.
refreshToken
is a one-time token with an extended expiration date. It is designed for updating a token pair when accessToken
expires. We store it in cookies, preferably with Secure and HttpOnly flags.
Now, let's imagine that the attacker got a hold of the accessToken. In this case, they will only get access for the lifetime of the accesToken
, and after it expires, they won't be able to get a new one.
What happens if the attacker takes over the refreshToken
as well? After all, he will be able to update a pair of tokens and access the victim's account for an indefinite period. That is not entirely true because refreshToken
should be a one-time use, so the server should terminate absolutely all user sessions when trying to reuse it.
After receiving a new pair of tokens, the attacker will get access until the real user loads the web application. When loading the app, the real user will also try to get a new pair of tokens. But someone has already used refreshToken
, hence all sessions will stop, the attacker will lose access, and the real user will be authorized once they enter their username and password.
Login and Password Theft
Unfortunately, the username and password could also be compromised. It might seem as if this is the users’ problem, and they must take care of their security. An average user, however, tends to create an oversimplified password and use it over and over again.
How to secure a user's account if the attacker has obtained the password?
It is easy to prevent users from entering simple passwords. You need to require the passwords to contain not fewer than eight characters as well as capital and lowercase letters, numbers, and special characters. However, these requirements may reduce conversion, so you can offer users an alternative to protect their sensitive data — a two-factor authentication.
Two-factor authentication
After the user enters the correct username and password, we generate a unique code and send it to the user's phone number or email address. The user then will need to enter this code into the login form. As a result, the attacker won't get access, and the real user finds out about the theft of the password and is able to enter a new one.
Limit password entry attempts
To protect against password guessing, we can limit the number of requests and then require a reCaptcha solution. This way, we will defend ourselves from automated password guessing.
Notifying users when they log in to their account
We can always inform the user when someone tries to log in using their username and password. It's enough to send an email or a text message for this. If the user was the one to log in, they could simply ignore the notification. If they don’t know who it was, they could quickly terminate the attacker's session by changing their password to prevent unauthorized access.
Secure authorization using the one-time generated code
A convenient and accessible method is when the user enters a phone number to log in and gets a text message with a one-time authorization code. But when implementing this approach, you must always remember about security. Here is why.
Often, for the users' convenience, one-time codes are made short and consist of numbers.
Let's imagine that our code consists of 4 digits and is valid for 10 minutes. That is 10,000 possible combinations. After entering the victim's phone number, the attacker starts guessing the code using a special program. The program sends ten requests per second, and as a result, they will check 6,000 combinations in 10 minutes. This means that there is a 60% chance that the attacker will be able to guess the code and log in.
What should you pay attention to when creating log-ins with one-time codes?
The longer and more complex the generated code, the less likely it is that an attacker could guess it. Using letters significantly reduces the chance of guessing. Complex codes, however, are inconvenient for users, so you’d have to find some balance between security and convenience. Based on our experience, a 6-digit code is enough, especially if you implement the following recommendations.
Related: Passwordless Authentication: What It Is and How It Works
It is important to limit the number of attempts to enter the code. It means that the user should only have 5-10 attempts to enter the code correctly. This amount is enough for the user to correct the possible typos and log in without problems. Meanwhile, hackers get a considerable challenge — now they only have ten attempts to guess the code, which is a 0.1% chance (with a 4-digit code). After ten failed input attempts, the attacker has to request a new code, but the requests are limited to one code per minute. As a result, they receive ten codes in 10 minutes and make ten attempts to guess each of them, but since the code is continuously updated, the chance of guessing remains at 0.1%. Thus, we reduced the possibility of unsanctioned authorization from 60% to 0.1% without creating additional complications for the users. And by extending our code to 6 digits, we reduce the probability to 0.001%.
Tips for Web Application Managers
The application manager is in charge of the whole application lifecycle from the IT perspective. Therefore, the manager should be the first one to raise the issue of security.
Here are some tips for web application managers to help develop a product that meets security requirements:
- You need to find out the business processes and security requirements of the web application. We are not talking about problems such as Injection, XSS, CSRF, etc. — it is obvious that these vulnerabilities should not be allowed in the development of any application. What we mean is the authorization method, its complexity, possible user roles, rights, and so on.
- You must add information about security requirements with a detailed description of user access levels to the technical specification.
- All security requirements must be discussed with the development team before the application is designed. If the requirements are complex, you should invite a security expert to the team.
- At the end of each sprint, you need to ensure that the developed functionality meets the security requirements and access to it is carried out within the previously described access levels.
- Conflicts between security and usability must be resolved promptly. For example, in a fintech application, a transfer to another user's account should be additionally secured by sending a message with a one-time code. At the same time, a transfer to another account of the same user can be performed without such confirmation.
To Sum Up
There are many well-known security problems on the modern web. Every programmer and application manager should know how they work and how to prevent the most common kinds of attacks. Such issues do not have to be described in the requirements specification. It is assumed that the programmer must write code that won't allow such security holes.
But some measures require approval because they affect usability. Such decisions must be made by the application manager and described in the requirements specification.
Keep in mind that not all applications require a serious security level, and basic measures are often sufficient. Any security issues should be discussed before you start developing an application because they can significantly affect the architecture or your technology choice.
If you have any questions or need advice, contact our team, and we will provide you with an expert opinion.