This course path focuses on OWASP Top 10 Risks & Common Attack Vectors (6-10), serving as a continuation of the foundational path OWASP Top 10 Common Attack Vectors with TypeScript (1-5). While the previous path covered the first five critical security risks, this path concentrates primarily on:
- A07: Identification and Authentication Failures - Covered comprehensively across authentication and MFA courses
- A08: Software and Data Integrity Failures - Addressed through data handling and web resource integrity courses
- A10: Server-Side Request Forgery (SSRF) - Dedicated course for prevention techniques
We also touch on A06: Vulnerable and Outdated Components and A09: Security Logging and Monitoring Failures where relevant. This path focuses specifically on the technical implementation aspects that software developers can directly control through code. While A06 and A09 also involve significant responsibilities for specialized roles like Application Security Engineers, DevOps Engineers, and Security Analysts, we concentrate on the developer-centric technical components rather than the broader organizational processes and infrastructure management these risks encompass.
Welcome to the very first lesson of the "Secure Authentication and Authorization in Express with TypeScript" course! In this lesson, we will explore the critical security measures of account lockout and enumeration prevention. These strategies are essential for protecting your web applications from unauthorized access and data breaches. By understanding and implementing these measures, you'll be better equipped to safeguard your applications against common attack vectors. Let's dive in! 🚀
As we have already encountered in previous courses, one of the most common vectors for attackers is stealing account credentials. Suppose the login page of an app has multiple fields and provides varying error messages when different credentials are entered. For example, it might sometimes indicate that the username does not exist, other times that the email is incorrect, and occasionally that the password is incorrect. This behavior can reveal that some credentials pass certain checks while others do not, inadvertently disclosing that a user is registered with a specific email. This issue can occur even with just username and password fields. If the application sometimes states that a username is not in the database and other times that the password is incorrect, an attacker can deduce the set of usernames registered in the app and subsequently attempt to discover the corresponding passwords. This is known as an enumeration attack.
Enumeration attacks involve discovering valid usernames or other sensitive information through error messages or response times. Attackers can use this information to gain unauthorized access to your application.
An attacker can easily exploit these vulnerabilities by manipulating login attempts or error messages. Here is an example:
-
Vulnerable Code:
In this example, the application returns a
404
status with a specific error message if the username does not exist and a401
status with a different message if the password is incorrect. This behavior allows attackers to determine which usernames are valid by observing the error messages, making the application vulnerable to enumeration attacks. -
Enumeration Attack:
In this script, the
curl
command is used to send a POST request, and the-w "%{http_code}"
option captures the HTTP status code. If the response code is401
, which typically indicates "Unauthorized" but may suggest a valid username with an incorrect password, the username is appended to . This way, the script logs valid usernames based on the server's response.
To prevent enumeration attacks, we need to ensure that our application does not reveal sensitive information through error messages or response times. Let's see how we can achieve this.
By using a generic error message like "Invalid credentials," we prevent attackers from determining whether a username exists in our system. This simple change can significantly reduce the risk of enumeration attacks.
Once attackers obtain some valid usernames, they may attempt a brute force attack. This involves systematically trying many passwords or passphrases until the correct one is found. Brute force attacks can be particularly damaging if there are no mechanisms in place to limit login attempts. Even with rate limiters, which restrict the number of attempts within a certain timeframe, attackers can circumvent these by distributing their attempts across multiple IP addresses or by pausing between attempts to avoid detection. This means that, over time, they could potentially discover valid credentials. Additionally, while rate limiting can mitigate some brute force attempts, it is often used in conjunction with account lockout mechanisms to provide a more robust defense. Account lockout temporarily disables an account after a certain number of failed attempts, adding an extra layer of protection against such attacks. Moreover, an account lockout mechanism can employ strategies such as increasing the lockout duration with each subsequent lockout or even implementing a permanent lock after a certain threshold is reached. A permanent lock might require additional verification, such as a successful multi-factor authentication (MFA) check, to unlock the account. These strategies help prevent slow attacks that attempt to bypass detection by pausing between attempts.
An attacker can exploit brute force vulnerabilities by automating login attempts. Here is an example:
- Brute Force Attack:
This script demonstrates how attackers can exploit vulnerabilities to gain unauthorized access.
To protect against brute force attacks, we can implement an account lockout mechanism. This involves temporarily disabling an account after a certain number of failed login attempts. Let's build this step-by-step in an Express application using TypeScript.
First, we need to track the number of failed login attempts for each user.
This code snippet initializes an object to store the number of failed attempts for each username. The key is the username, and the value is the count of failed attempts.
We'll implement the logic to lock an account after too many failed attempts.
In this code snippet, we first check if the account is already locked by verifying if there is an entry in the lockedAccounts
object for the given username. We also compare the current time (Date.now()
) with the lockout expiration time stored in lockedAccounts[username]
. If the current time is less than the expiration time, it means the account is still locked, and we return a 429
status code with an appropriate error message.
Here, we increment the count of failed login attempts for the username. We use the logical OR (||
) operator to initialize the count to 0
if it doesn't already exist, ensuring that the count starts from zero for new entries.
In this part, we check if the number of failed attempts has reached or exceeded the predefined LOCK_THRESHOLD
. If it has, we lock the account by setting an expiration time in the lockedAccounts
object, calculated as the current time plus the LOCK_DURATION
. We then return a status code with a message indicating that the account is temporarily locked.
Finally, we reset the failed attempts count after a successful login.
This ensures that users are not unfairly penalized for past failed attempts once they successfully log in.
In this lesson, we explored the vulnerabilities of enumeration and brute force attacks and learned how to implement defensive strategies to protect against them. By understanding these concepts and applying the techniques we've covered, you can enhance the security of your web applications.
As you move on to the practice exercises, you'll have the opportunity to apply what you've learned and reinforce your understanding. Keep exploring related security measures to build a comprehensive understanding of web application security. Good luck, and see you in the next lesson! 🎉
