It’s super important to consider the security of the applications you build. There are lots of ways to address security issues, but a powerful way to get started is to address the top ten security issues as identified by OWASP (the Open Web Application Security Project). In this article, we’ll walk through the current top ten security vulnerabilities for applications.
OWASP is an international organization dedicated to the security of web applications, and every four years, the community releases the OWASP Top 10 report, which outlines the most pressing security concerns for web applications. We’ll look at these vulnerabilities through the lens of a PHP developer, but they are relevant to building applications in any programming language.
OWASP Security Vulnerabilities: An Overview and Comparison
Table of Contents
- 1 OWASP Security Vulnerabilities: An Overview and Comparison
- 2 Broken Access Control
- 3 Cryptographic Failures
- 4 Injection and Insecure Design
- 5 Security Misconfiguration and Outdated Components
- 6 Identification and Authentication Failures
- 7 Software and Integrity Failures and Logging and Monitoring Issues
- 8 Server-side Request Forgery
- 9 Summary
The 2021 OWASP Top 10 list features ten of the most dangerous security vulnerabilities for web applications. If we compare the current list to the 2017 list, we can see that some security flaws remain in the list but are in a different place, and a couple of new security flaws are on the list as well.
Below is a table comparing the lists from 2017 and 2021. (The security flaws that were introduced to the 2021 list are outlined in bold, and the rest are just reshuffled.)
2017 OWASP Top 10 | 2021 OWASP Top 10 |
---|---|
#1 – Injection | #1 – Broken Access Control |
#2 – Broken Authentication | #2 – Cryptographic Failures |
#3 – Sensitive Data Exposure | #3 – Injection |
#4 – XML External Entities (XXE) | #4 – Insecure Design |
#5 – Broken Access Control | #5 – Security Misconfiguration |
#6 – Security Misconfiguration | #6 – Vulnerable and Outdated Components |
#7 – Cross-site Scripting (XSS) | #7 – Identification and Authentication Failures |
#8 – Insecure Deserialization | #8 – Software and Data Integrity Failures |
#9 – Using Components with Known Vulnerabilities | #9 – Security Logging and Monitoring Failures |
#10 – Insufficient Logging and Monitoring | #10 – Server-side Request Forgery (SSRF) |
This table suggests that the majority of the security flaws that target web applications don’t change. What changes is the approach of developers when they attempt to fix these flaws. Contrary to a popular belief, avoiding these security flaws is rather easy to begin with; we just have to know a couple of basic rules applicable to a specific security issue.
Let’s dig into each of these security issues.
Broken Access Control
According to the 2021 edition of OWASP, the issue we should be paying the most attention to is broken access control. Broken access control is just what it sounds like: it occurs when the way we control access to our applications is flawed. An example of broken access control is pictured below.
<form method="post" action="">
<input type="text" name="Username" placeholder="Your Username?">
<input type="text" name="Password" placeholder="Your Password?">
<input type="submit" name="Submit" value="Log In">
</form> <?php
if(isset($_POST['Submit'])) { $Username = $_POST['Username']; $Password = $_POST['Password']; if(!empty($Username)) { if(!empty($Password)) { header("loggedin_page.php"); exit; } }
}
?>
Do you see the problem? The code is simply checking whether the username and password fields are not empty. What about running a couple of queries in the database to ensure that the username and password exist? To verify the account in question? That part has been conveniently forgotten about. A user can simply put anything in the username and password fields to ensure they’re not empty, click Submit, and the user will be logged in.
To avoid broken access control issues: always verify the username (email) and password fields against the database before flagging the user as logged in.
Cryptographic Failures
Cryptographic failures were previously known as “sensitive data exposure”. Sensitive data exposure was renamed to “cryptographic failures” because this addresses a number of security problems, while “sensitive data exposure” addresses only one of them.
Cryptographic failures cover the failure of encrypting data, which often leads to sensitive data exposure. Cryptographic failures in PHP are mostly related to passwords: hashing them with anything other than hashing algorithms that were designed to be slow (think BCrypt and Blowfish) is a cryptographic failure, because other types of hashes (MD5 and similar) are easy and quick to bruteforce.
To avoid cryptographic failures: ensure that all of the passwords stored in your database are hashed with an algorithm that’s slow to bruteforce. We suggest you choose Blowfish or BCrypt, as these algorithms are safe for long-term use, have been tested by security experts, and have been proven to withstand attacks.
If you have a lot of users using your application, you might also want to look into salting. For large numbers of hashes, salts slow down the cracking process.
Injection and Insecure Design
Injection is the most frequently discussed security issue on the Web. Everyone has heard of it: pass user input to a database, and you have an injection flaw. Injection attacks are relatively simple to overcome, but they’re still a problem due to the sheer amount of applications connected to databases.
The image below depicts a relevant code example.
<form method="post" action="">
<input type="text" name="Username" placeholder="Your Username?">
<input type="text" name="Password" placeholder="Your Password?">
<input type="submit" name="Submit" value="Log In">
</form> <?php
if(isset($_POST['Submit'])) { $Username = $_POST['Username']; $Password = $_POST['Password']; if(!empty($Username)) { if(!empty($Password)) {
$Query = $DB->query("SELECT * FROM users WHERE username = $Username AND password = $Password"); } else { echo "Password empty!"; exit; } } else { echo "Username empty!"; exit; }
}
?>
The flaw shown above is pretty self-explanatory: when any user input is passed on to a database, anyone can do whatever crosses their mind. This flaw is not exclusive to PHP. If you pass user input straight into a database using any other programming language, and you’ll have exactly the same problem.
The consequences of a successfully mounted SQL injection attack can range widely, but in most cases, they span the following things:
- The attacker can take a backup copy of the user table to perform credential-stuffing attacks on other information systems.
- The attacker can gain administrative rights inside the database, then modify or delete tables within it.
Both of these actions, if performed successfully, will be detrimental to any business. A database dump taken of the user table will result in it being sold on the dark Web, and once it sells and the attacker makes a profit, other attackers will use the data to mount credential stuffing attacks. An attacker gaining administrative rights to a database that stores user data will also result in havoc — not just for site users, but for the site owners, who’ll be hit with a storm of negative public scrutiny.
To avoid SQL injections: use PDO with parameterized queries. Such an approach protects applications against SQL injection, because the data is sent separately from the query itself.
Such an approach to the query shown earlier would look as pictured below (notice the changes in lines 13 and 14).
<?php
if(isset($_POST['Submit'])) { $Username = $_POST['Username']; $Password = $_POST['Password']; if(!empty($Username)) { if(!empty($Password)) { $Query = $DB->prepare("SELECT * FROM users WHERE username = :Username AND password = :Password"); $Query->execute(array(":Username" => $Username, ":Password" => $Password)); } else { echo "Password empty!"; exit; } } else { echo "Username empty!"; exit; }
}
?>
Insecure design, on the other hand, differs from injection and has a separate category. Injection is part of insecure design, but insecure design isn’t injection. Insecure design covers how code is written by design (that is, by default). This means that if, by default, your code passes any of the user input to a database, or if it allows users to log in without authenticating themselves, or if it allows them to upload files without checking their extensions, or if it returns user input without validating it, you have an insecure design flaw.
To avoid SQL injection, passing user input to a database, and insecure design flaws, ensure that you follow secure coding guidelines outlined by OWASP or other vendors. If you follow these guidelines, you should be safe on this front.
Security Misconfiguration and Outdated Components
In the fifth and sixth spots, we have security misconfiguration and outdated components. These two flaws are different from those mentioned previously, but they’re also very dangerous.
When probing an application for a possible security misconfiguration vulnerability, attackers will look at everything. They’ll attempt to access default accounts, access pages that should be protected, exploit unpatched vulnerabilities, and so on. Our only hope in this scenario is for the components to be updated and patched against all kinds of vulnerabilities. Outdated components often come with nasty vulnerabilities which, if exploited, can result in a database being leaked and sensitive data being exposed, servers going down, reputations lost, fines being issued, and so on.
That’s why it’s vital to always do the following:
- Ensure that your application uses components that are always up to date.
- Make sure to forcibly log out users after a certain period of inactivity. (That is, make sure that sessions expire after a specified period of time.)
- If possible, consider implementing a CAPTCHA after a certain period of unsuccessful attempts to submit a form, or to log in to a part of a website, and so on.
- If possible, use a web application firewall to protect your web application from attacks directed at it, and consider using services like the one provided by Cloudflare to protect your application against DoS and DDoS attacks at the same time.
To avoid misconfigurations and outdated component flaws: ensure that you’re using updated components and that your code follows basic security standards such as those mentioned above.
For your application to be more secure, pay close attention in particular to the components that let users authenticate themselves.
Identification and Authentication Failures
Identification and authentication failures were previously known as the “Broken Authentication” vulnerability. Such a vulnerability occurs when an application doesn’t adequately protect the part of itself that lets users authenticate themselves, which could mean one or more of the following things:
- The application doesn’t protect its forms from bruteforce attempts by using a CAPTCHA or via other measures.
- The registration page of the application permits weak passwords to be used. (That is, the application doesn’t have a minimum password length defined.)
- The registration form lacks a “repeat password” field. (That is, users are registered without double-checking whether their password is correct.)
- The password-changing form is not protected from CSRF (cross-site request forgery), letting a user B forge requests on behalf of user A. (That is, a user B could send a specifically crafted URL that, upon opening, would change the password of user A.)
- Accounts can be enumerated: the application provides different kinds of messages depending on whether a certain account exists in a database or not.
- The application is storing passwords in plain text.
- The application returns the username after it’s specified in an input parameter without filtering it. (Such an approach enables an XSS attack, where an attacker is able to inject malicious scripts into a website.)
To avoid identification and authentication failures: make sure the register and login forms are built securely. Of course, that’s easier said than done, but follow the steps outlined below, and you should be good to go:
- Ensure that all registered users use safe passwords. (Enforce a policy of eight characters or more.)
- Present users with a CAPTCHA after a set number of unsuccessful login attempts (say, five or more). In other words, enforce rate limiting.
- Make sure that all parameters presented by a user are clean. (That is, don’t return user input back to the user without validating. Doing so will enable an XSS attack.)
- Make sure the form allowing passwords to be changed is secured against CSRF. In other words, generate a token that changes on every request to make an attacker unable to forge a request and pose as a user.
- Use two-factor authentication wherever possible to avoid credential-stuffing attacks targeting your login form.
Software and Integrity Failures and Logging and Monitoring Issues
While issues related to the logging and monitoring mechanism are relatively self-explanatory, software and integrity failures may not be. There’s nothing magical about this, though: the OWASP community is simply telling us that we should verify the integrity of all kinds of software we’re using, be it PHP-based or not. Think about it: when was the last time you updated your application? Did you verify the integrity of the update? What about the assets you load into your web application?
Take a look at the code example pictured below. Do you notice anything?
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAg046MgnOM80zWlRWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> <form method="post" action="">
<input type="text" name="Username" placeholder="Your Username?">
<input type="text" name="Password" placeholder="Your Password?">
<input type="submit" name="Submit" value="Log In">
</form> <?php
if(isset($_POST['Submit'])) { $Username = $_POST['Username']; $Password = $_POST['Password']; if(!empty($Username)) { if(!empty($Password)) { $Query = $DB->prepare("SELECT * FROM users WHERE username = :Username AND password = :Password"); $Query->execute(array(":Username" => $Username, ":Password" => $Password)); } else { echo "Password empty!"; exit; } } else { echo "Username empty!"; exit; }
}
?> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-kenUlKFdBIe4zVFOs0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
Both the style sheet and the JavaScript file loaded into it are protected by an integrity attribute. The integrity attribute is vital for ensuring the style sheets and JavaScript files loaded into a web application haven’t been tampered with. If the file’s code has changed at all from when the integrity attribute was first generated, the scripts won’t load into the web application, and instead we’ll be presented with an error, as pictured below.
Integrity is everywhere. And while in some situations a violation of integrity will provide warnings (as in the image above), in other cases the consequences will be more severe and can lead to a data breach.
To make sure your code hasn’t been tampered with, it’s critically important to properly monitor your infrastructure, but such an approach can itself be flawed. When your approach to monitoring is flawed, you won’t catch errors. Some errors are small, like the one shown above. Some errors are more severe, such as when your application is vulnerable to SQL injection, security misconfiguration, or any other of the other flaws listed above. So always make sure that your application is logging any anomalies related to the critical functionality of your website.
Doing so is easier said than done, but employing software that monitors the entire perimeter of your website for unauthorized, security-related events is an excellent place to start. Unfortunately, monitoring everything manually is only feasible when your application is rather small, but it won’t get you far in the long run.
To mitigate software and integrity failures, and logging and monitoring issues: look into web application firewall offerings such as those offered by Cloudflare, Sucuri, or Imperva. And remember: paying security vendors is always cheaper than recovering from a data breach.
Server-side Request Forgery
The final important issue in the 2021 OWASP Top 10 list is server-side request forgery (SSRF). SSRF is an attack that allows a nefarious party to send requests to a website through a vulnerable server. SSRF is a new vulnerability in the OWASP list, and it acts similarly to its CSRF cousin. While CSRF aims to make unintended requests on behalf of the user, SSRF aims at the server. SSRF forces an application to make requests to the location set by the attacker. Look at the piece of code pictured below. (Note that the call to the header()
function should be completed before any text is written into the page, as otherwise the call might be ignored.)
<?php
if(isset ($_POST['Submit'])) { $URL = $_GET('picture_url']; if(filter_var($URL, FILTER_VALIDATE_URL)) { $Contents = file_get_contents($URL); header("Content-Type: image/png"); echo $Contents; header ("Location:picture_changed.php"); exit; } else { echo "Incorrect URL."; exit; }
}
?>
This piece of code does a couple of things. It first checks whether the URL provided in the picture_url
GET parameter is a valid URL. It then provides the content in the URL to the user, and redirects the user to another PHP script. Providing the content in the URL to the user is precisely what makes this PHP code susceptible to SSRF. Displaying anything provided by the user is dangerous, because a user can do any of the following things:
- Provide a URL to an internal file on the server and read sensitive information. For example, when a URL like
file:///etc/passwd/
is provided, an application susceptible to SSRF will display the/etc/passwd
file on the screen. But this file contains information about users that own processes running on the server. - Provide the app with a URL of the file on the server, then read the file. (Think in terms of providing a URL to a web service on the server, and so on.)
- Provide the app with a URL to a phishing page, then forward it to a user. Since the phishing page would reside on the original URL (the URL of your server), there’s a high chance an unsuspecting user might fall for such a trick.
Avoiding SSRF: Arguably, the easiest way to avoid such an attack is to employ a whitelist of URLs that can be used. A whitelist in a PHP web application would look something along the lines of the code pictured below. (Notice particularly lines 25 to 28.)
<?php
if(isset ($_POST['Submit'])) { $URL = $_GET('picture_url']; $whitelist = array("https://google.com", "https://twitter.com", ""..."); if(!in_array($URL, $whitelist)) { echo "Incorrect URL."; } if(filter_var($URL, FILTER_VALIDATE_URL)) { $Contents = file_get_contents($URL); header("Content-Type: image/png"); echo $Contents; header ("Location:picture_changed.php"); exit; } else { echo "Incorrect URL."; exit; }
}
?>
</div>
</form>
Now the application echoing the output of the URL back to the user is no longer a problem, since the list of URLs is controlled by you. Your application is no longer vulnerable to SSRF!
Summary
In this article, we’ve walked you through the top ten security flaws that could compromise your PHP web applications. Some of these flaws entered OWASP for the first time in 2021, and others have been reshuffled from the older 2017 edition of OWASP. However, one principle remains the same: all of them are relatively dangerous and should be dealt with appropriately.
Hopefully you’ve found this article useful for improving the security of your applications. To dive deeper, I recommend you immerse yourself in the world of OWASP to learn more about how to best secure your web applications.