Sometimes as developers, it can be difficult to keep up with the ever-changing frameworks, tools, and releases. You spend a lot of time fixing bugs, refactoring, and frankly just trying to get your code to run. Understandably, a lot of things get pushed to the lengthy backlog list. But should your application's security ever end up on that list?
The short answer is "no." So how can you keep up with all of your other tasks while also making security a priority? The easiest way is to take some time to understand common vulnerabilities and then make prevention a part of the development process. The form that you built works, but leaves you open to an attack? Then it doesn't work.
Luckily, some of the most common attacks don't actually require much extra work to prevent.
In this article, I'll go over some of the most common vulnerabilities that directly affect developers and how you can make simple changes to prevent them. This is not an exhaustive list, but it's a great place to get started. If you'd like a follow-up to learn about even more vulnerabilities, leave a comment below and let me know.
If you're on desktop, feel free to use the menu to the left to skip around to the vulnerabilities that interest you. Let's get started!
You have probably heard of SQL injection, one of the most common attacks, but there are a few other types of injection that attackers can exploit. All of these attacks stem from one thing: not handling user-submitted data correctly. Let's look at a couple of examples of injection attacks.
To see how easy it is to exploit a vulnerable application with server-side code injection, take a look at the PHP
eval() — PHP function that evaluates a string as PHP code
This function can take user-submitted input and run it as code. This is very dangerous, especially if the proper precautions aren't taken. Let's take a look at a hypothetical calculator application. The calculator lets the user input some equation, and then it will spit out the result.
$userExpression = '5+3'; echo eval('return ' . $userExpression . ';');
Great! The user submitted their input, the calculator used
eval() to solve the equation, and then returned the correct value: 8.
But what happens when a not so friendly user decides they want a little more from the calculator?
$userExpression = 'phpInfo();5+3'; echo eval('return ' . $userExpression . ';');
Since there is no input validation, the user can type in anything they want, and it will be evaluated and ran as PHP code. You can try it out for yourself using a PHP sandbox, and you'll see that the PHP configuration information is printed out.
An attacker would be able to wreak havoc on your application if they could run any code they'd like. We'll go over some prevention techniques shortly, but a good start is to never directly execute input from users.
SQL injection (SQLi) is another common attack that can be easily prevented but also easily overlooked in the development process.
To execute an SQLi attack, the user would add an SQL query to the input that they're sending to the application. If the application had no safeguards against this, it would execute the user-submitted query, which could cause massive destruction to the application's database.
A common example of this attack is someone gaining access to user credentials by running an attack on the login form. This would, of course, be devastating, but what if the attacker targets some data that might not seem as sensitive? Maybe when the developer created the form, it didn't occur to them to add as much protection to it because it doesn't carry the severity of leaked user credentials. This can still be devastating. Let's take a look.
Imagine you have a website that lets users check their order information by entering the order number and their email address into a form.
The query to grab the order information based on the form inputs looks something like this:
SELECT * FROM orders WHERE $email=$_POST['email'] AND $id=$_POST['order_number'];
The user enters their order number and email, and then the query is executed with those inputs to pull the data from the database.
But now a not so friendly user comes along and decides they want to pull everyone's orders. Instead of just typing in their email, they type in these values as input:
firstname.lastname@example.org' OR 1 = 1 -- ']
Now your application grabs that input and just drops it right into the SQL string. This is what gets executed:
SELECT * FROM orders WHERE email@example.com' OR 1 = 1 -- '] AND id=1;
And the result? The attacker now has EVERYONE'S order information. This could mean names, emails, addresses, and more.
So what happened? Since whatever the end-user typed into the input box was executed as SQL, they were able to chain an extra conditional to the query,
1 = 1, which always evaluates to true. The rest of the query does contain an
AND operator, but the attacker commented that out. Even if they didn't add the comment at the end, they could still pass in any order number and receive the data for it even though they don't have the correct email.
Attacks in the news
In 2018, Lenovo experienced a data breach that affected over 1 million customers. The hackers were able to get full access to Lenovo's website using an SQL injection attack.
"...the hackers were able to find a product ID online which accidentally led them to an error page. Then, using this error page, hackers proceeded to enter a series of query strings ultimately granting them full administrator level access over the website and all its contents – allegedly over 20 GB of data."
SQL injection isn't limited to just stealing a database. Another concern is data integrity. If an attacker has access to a database, they can cause a lot of damage by modifying the data in it. But what could an attacker possibly gain from changing data? Consider these two hypothetical scenarios:
An attacker gains access to a healthcare database that contains sensitive user information. They alter this information and hold onto the original database. The healthcare company might not even notice the changes until it's too late. The attacker then demands a ransom before they will revert the changes.
An attacker accesses the database of a popular cryptocurrency exchange. They manipulate the value of a currency that's displayed on the website. This could lead to mass buy-offs or sell-offs, which could ultimately put a lot of money into the attacker's pockets, depending on their goals.
So as you can see, leaving holes in your application that can lead to data infiltration can cause damage in several ways.
Luckily, avoiding these scenarios is fairly straightforward. The methods used to protect against both SQL injection and code injection are largely the same. You need to focus on two things:
- Always validate and sanitize user input
- Do not directly execute user input
The first thing you should do is validate any input that's coming in from a user. If you expect the input to be an integer, then make sure you check for that. If you expect it to be in a certain format, check for that. Most languages have built-in validation rules or packages you can add to simplify this process.
And most importantly, if you receive some input that isn't what you expect, reject it and send back an error message. You might decide to be clever and try to just modify the input to remove dangerous characters, but there is way too much that can go wrong with this.
For example, if you're expecting a string, you may decide you want to strip out any
</script> tags and then trust the remaining string. But what if an attacker does something like this:
When you parse the string and remove the opening and closing
The second part of prevention is to never directly execute user input.
The most common method to avoid this in an SQL query is to use prepared statements or parameterized queries. Let's look at what this means in the context of the SQL injection example above.
SELECT * FROM orders WHERE $email=$_POST['email'] AND $id=$_POST['order_number'];
Instead of directly inserting the user input into the query as shown above, you could create a parameterized query like this:
// create mysql connection $db_connection = new mysqli('host', 'user', 'password', 'db'); // user input $email = $_POST['email']; $id = $_POST['order_number']; // create the prepared statement $ps = $db_connection->prepare("SELECT * FROM orders WHERE email = ? AND id = ?"); $ps->bind_param("si", $email, $id); $ps->execute();
This statement defines a template that will be used for the query. The query is defined before the statement is executed. The
si parameter in
bind_param() signifies that a string and an integer are expected. Then the
id parameters will properly escape and replace the question marks in the query so that they can't be executed as SQL. In the attack above, it would turn into:
SELECT * FROM orders WHERE email = 'firstname.lastname@example.org OR 1 = 1 -- ]' AND id = 1;
And just like that, you prevented an SQL injection.
Cross-Site Scripting (XSS)
Cross-site scripting, or XSS, is a vulnerability that allows attackers to send malicious code, usually through a form or URL, that will run on an end user's browser. This is technically a type of injection, but the consequences of this attack are a little different since it's a client-side attack.
The big difference here is that if someone successfully performs an XSS attack on a vulnerable application, they're able to access a legitimate user's data and browser information instead of just the application data alone. Let's take a look at how this can happen.
Say you're searching around for the best bank to get a credit card from. You come across a personal finance forum, and someone recommends a well-known bank with a link to the bank's website, which you know to be legitimate. You quickly glance at the link, see
https://legit-bank.com, and click on it without reading any further.
This is the actual URL that you clicked:
You land on the page and get an alert popup with the message "BOOM!". So what happened?
In this hypothetical scenario, the attacker found a vulnerable page on the bank website: the search page. They typed
<script>alert('BOOM!')</script> into the search box, the application prepared the query using the input as a string, executed the search, and then sent back a message that no results were found. The developers knew to sanitize the input on the backend before running the query, so then what went wrong?
The application sent back the message that no results were found, but also dumped the exact query inputted by the user onto the page. If this were a string, as expected, there would be no problem. But since it's a script, it executes on the page.
Now whoever clicks this link will be affected by this rogue script. Of course, an alert popup is mostly just nuisance, but XSS attacks can be far more detrimental.
In some applications, the attacker would be able to use that session cookie they stole to impersonate the user on the bank website. They wouldn't even need their username and password because the existence of the cookie, from the application's perspective, means that the user has already signed in!
This is known as session hijacking. This is the basic flow:
- User logs into their bank and receives a session cookie that tells the application who the user is
- The user clicks on the compromised link
- The attacker grabs the session cookie and begins making requests to the bank and sending that cookie along with the request
- The attacker now has access to that user's bank account
This attack isn't limited to just URL sharing, either. If the attacker finds a vulnerable form that allows them to write to a database, they can drop their malicious script there as well. Imagine you have a vulnerable commenting system that doesn't validate the data coming through. When building the page, you just hit the database for all of the comments for a specific blog post and dump them exactly as they're returned from the database. The attacker can write a comment that holds the script, submit it, and then any time a user goes to that blog post where the comment is, the script will execute.
Just like you saw with injection attacks, the best way to prevent XSS attacks is to view all user input as untrustworthy and protect against it accordingly.
Here's how you'd do that:
- Validate all incoming data
- Escape or sanitize user-submitted data that will be rendered on the page, e.g., blog comments
Data validation, as mentioned earlier, just ensures that the data submitted matches the format that's expected. In the previous examples with the bank search and commenting system, proper data validation could have helped prevent an attack by only allowing letters and numbers as input, no special characters. As soon as
<script> was detected, the query would have been rejected.
Escaping input is also necessary, especially if you're going to allow user-submitted input to be rendered to the page. This just means changing the special characters, such as
<, to their hex code,
<, so that they can't be read as executable code.
If the example above had been escaped before rendering, it would look like this when displayed on the page:
Most modern frontend frameworks escape data by default before rendering, which is great. Just make sure you're aware of this magic in case there comes a time when you're not using a framework, and you have to do it on your own.
Attacks in the news
Back in 2014, Tweetdeck, owned by Twitter, was hit by a massive XSS attack that stemmed from a single tweet. The app failed to escape data properly, so the script that was tweeted out was actually executed and caused anyone who viewed the tweet to automatically retweet it as well. This spread like wildfire, and although this was a mostly innocent example, it could have had disastrous effects if used by someone maliciously.
<script class=“xss“>$('.xss').parents().eq(1).find('a').eq(1).click();$('[data-action=retweet]').click();alert('XSS in Tweetdeck')</script>♥
— *andy🚀 (@derGeruhn) June 11, 2014
If you're curious, above is the offending tweet. But don't worry; the XSS vulnerability has been fixed now!
This next one is a little tougher to deal with, but very important. If you're integrating any third-party software or packages into your application, you need to put them under just as much scrutiny as your own codebase. There are a lot of breaches that occurred not due to the company's codebase, but because the attacker infiltrated one of the third-parties used by the affected company and was able to access the company's data. Let's look at some examples of how this can happen.
Think back to the last project you worked on. Did you use any external packages? Chances are you did. If you're unsure, take a peek at your
node_modules folder. Massive, right? You probably only installed a few packages on your own, but all of those packages also have dependencies. And those dependencies have dependencies. So how can you be so sure that all of the hundreds of packages installed are free from vulnerabilities?
While nothing in this scenario is bulletproof, there are some precautions you can take to greatly reduce your chances of creating vulnerabilities when including third-party packages in your applications.
The first step is to make sure you trust a package before installing it. There is always going to be risk when using a package that contains code written by someone else, so you have to determine the amount of risk you're willing to take. Ideally, you'd want to manually review every file in every package you install. A lot of companies have security teams to do this, but of course, this isn't always feasible for a single developer or a small team. Here are some things to consider when assessing the risk of a package. Is it open source? If not, is it from a reputable company? Is it popular and actively vetted by the developer community? Are there any open issues on GitHub that indicate that it could be malicious?
Even if a package is open source and has thousands of stars on GitHub, it doesn't necessarily mean you can trust it. Oftentimes, a closed source package from a big company will carry less risk than an open source one from an independent developer. One benefit of a popular open source package is that it will have a lot of eyes on it, but again, this only reduces the risk factor.
Once you've decided you can trust the package, there are still a few more things you need to do before you can feel confident that it's safe.
First, when you're typing in the package name, triple-check that you've spelled it correctly! Believe it or not, there is something called typosquatting where attackers create a malicious package and then give it a name similar to a legitimate and popular package. The Python Package Index, PyPI, has been found to have several malicious typosquatted packages throughout the years.
So if you accidentally typed in
pip install djago instead of
pip install django, no error would have been thrown as the package, though malicious, did exist. Npm has experienced the same issues as well.
Alright, so you've vetted the package, you've typed it in correctly during install, now what? Now you need to make sure you stay up to date with security patches! Npm makes this fairly easy. Every time you run
npm install, an audit is run, which checks for security vulnerabilities in all of your project's Node modules. In an existing codebase, you can run
npm audit at any time to run this same check.
If you get a message in your terminal that some of your packages include vulnerabilities, do take some time to review them and update them as needed. You can even run
npm audit fix to automatically install the necessary updates. If you do go this route, make sure you're aware of any breaking changes that come along with updating the package. Npm will warn you of this.
To sum it up, these are some precautions you should take when using third-party packages:
- Keep your packages up to date
- Make sure that you trust the package, the package owners, and the maintenance of the package
- Double-check for typos when you install a package
npm auditon existing projects
It's a bummer that this much care has to be taken when downloading third-party packages, but the threat is very real. Remember the
event-stream package fiasco back in 2018? The popular package with over 1 million downloads per week was passed off to a new owner who seemed reputable, as they had heavily contributed to the package in the past. Unfortunately, the new owner had ulterior motives all along. Once they gained the owner's trust and received full control of the package, they modified it to attack the Bitcoin wallets of anyone who downloaded it. This went undetected for two months until someone from the community noticed it and opened an issue.
Just to drive the point home, take a look at what happened to Equifax when they neglected to update an external package that was known to have a critical vulnerability. The vulnerability came to their attention quickly, but they put off patching it for months. In that time, attackers were able to exploit that vulnerability and steal the data of 143 million people. So while it may seem tedious and time-consuming to pay such close attention to third-party packages, it could potentially save your company hundreds of millions of dollars in legal fees.
Another important and related topic worth a mention is the use of third-party vendors. This might not directly affect your work as a developer, but you should still be aware of the security risks that come along with sharing your data or granting network access to a third-party.
If an attacker is looking for a way in, they're most likely going to go for the weakest link. In the case of the 2013 Target breach, which resulted in the theft of the credit and debit card numbers of 40 million people, the weakest link was their HVAC vendor. The attackers were able to steal network credentials from the HVAC company and then gain access to Target's network.
If you're going to work with third-party vendors, make sure you do a full audit of their security practices. You should also make sure you're only giving them access to the data they need. The more access you give them, the more detrimental a potential breach will be. This may fall outside your authority as a developer, as it's a tedious and in-depth process, but if you know your company is working with a vendor, it's important to have these checks in place. You can always hire an external company that specializes in vendor auditing if you don't have a team to do it.
Mishandling Sensitive Data
Next up is another often overlooked vulnerability: mishandling sensitive data.
I was recently chatting with someone who works in anomaly detection, and they mentioned that it's fairly common to see developers committing sensitive information to GitHub repositories. I was skeptical, so I did my own digging using the GitHub search feature and was pretty shocked by all of the sensitive information I found in public repos. Let's go over some scenarios where you might accidentally expose sensitive data.
Committing keys to GitHub
As mentioned before, it's surprisingly common to see developers committing API keys and credentials to their GitHub repos. Of course, this is bad because that information is meant to be secret, but how do you use that information throughout a public repo without committing the keys or credentials themselves?
One common way to deal with this is to use
ENV variables. The basic idea is you create a file called
.env and store all of your secrets in there under a variable name.
Then when you need to use it in your application, you just would use the variable from the
.env file similar to this:
process.env.AWS_KEY. This is how it's used with the popular
dotenv Node package, but the actual usage will vary depending on the language.
The most important part is that you add this file to your
.gitignore! Then when you push your code, none of your secrets are saved into the repository, just the references to them. Once you're ready to deploy, you can store this file securely on your server.
There are also tools out there, such as the Auth0 Repo Supervisor, that can warn you in case you accidentally try to commit sensitive data.
Error messages and logging
The next issue that's surprisingly common to see is developers including sensitive data in their error messages or logs. In some extreme cases, authorization isn't even required to view these logs.
Logs can help you find out what went wrong in the case of a breach or error in your application, but ironically, they can also become a vulnerability if you start to log sensitive information such as credentials, user data, or information about your infrastructure.
If your logs are accessed by an unauthorized third-party, you don't want them to have a goldmine of data. In general, it's best to keep sensitive data out of your logs altogether. Here is a great resource on best practices for using logs in your application.
Not handling error messages correctly is another common issue that can lead to an attack. If someone is poking around your site, they can try different tactics and then adjust based on the error message that comes back. Linking back to SQL injections, an attacker could enter in different queries to an already vulnerable application and then, based on the messages returned, determine how the query and even the entire database are structured.
Here are some guidelines to follow when writing your error messages:
- Do not reveal sensitive data in the message
- Keep them generic — don't give away too much about the inner-workings of your application
- Make sure you don't let any errors go unhandled
try/catchblocks when possible
Again, the main goal here is to make sure you're not inadvertently giving the attacker a manual to your application through your logs or error messages.
One area that is especially coveted by attackers is the login form. If error messages are not properly handled where authentication is concerned, attackers can begin chipping away at your application's inner-workings and eventually find a way in. At Auth0, we can provide a Universal login solution so that your login form, and therefore any threat prevention, is handled by us.
Why This Matters
Clearly, there's a lot that can go wrong if you decide to put application security on the back-burner, but why should you care? It turns out, data privacy is very important. The EU and California have already put extensive regulations into place to protect users and penalize companies in case of a data breach.
The General Data Protection Regulation (GDPR) calls for accountability and transparency when it comes to user data. Although it was drafted in the EU, it applies to any company that holds the data of EU residents, regardless of where the company is headquartered.
British Airways was hit with a $228 million fine after nearly half a million users had their credit card information, personal information, and login credentials stolen during checkout.
This isn't limited to huge companies either. In Portugal, a hospital was fined half a million dollars because they didn't properly manage their administrator's accounts and authorization. The financial consequences of a breach can be devastating to a small or medium-sized company.
The state of California has released its own data privacy law, the California Consumer Privacy Act (CCPA), which went into effect on January 1, 2020.
These new regulations represent the beginning of a shift in how we view data privacy. Protecting your user data is only going to become more important in the future, so you should get ahead of the curve and make security a priority now.
How We Can Help
This is by no means an exhaustive list, although keeping up with all of this is exhausting.
You might have noticed that this post neglects the mention of authentication and authorization implementation. This isn't because it's not important. It's because there are so many things that can go wrong that I couldn't possibly list all of them here. In fact, it consistently ends up on the OWASP Top 10 Most Critical Web Application Security Risks list year after year. And unlike the vulnerabilities mentioned above, broken authentication and/or authorization requires a lot of specialized knowledge, strategy, and time to fix.
At Auth0, our universal authentication and authorization platform can help take some of the burden off of you and your team so that you can focus on building cool features for your application.
With Universal Login, your users will be redirected to our central authorization server to sign in. So instead of building a login form on your own and worrying about preventing SQL injection and other attacks, we'll do all of that for you.
You can sign up for a free Auth0 account here to take a peek at the dashboard immediately. We also offer several quickstarts for most languages and frameworks to get your application up and running in minutes.
Earlier, I recommended heavy vetting when using a third-party service, and this is no exception. But rest assured, we take security and compliance very seriously. If you have any questions, feel free to reach out to us through the chat popup on this page or the "Talk to sales" button in the top right corner.
Since you're interested in the security of your application, here are some other protective perks of using Auth0:
- We handle your user database for you
- Enable Multi-factor authentication with one click
- Automatically identify suspicious activity with our anomaly detection tools
Part of our anomaly detection suite includes:
Brute force protection
In a brute force attack, the attacker will attempt to sign in to an account by repeatedly entering in guesses of the password until the correct one is found. Our brute force protection is turned on by default and will trigger under the following conditions:
- 10 consecutive failed login attempts for the same user and from the same IP address
- 100 failed login attempts from the same IP address in 24 hours or 50 sign up attempts per minute from the same IP address
When user login credentials are stolen in a data breach, they often end up sold to malicious actors. Surveys have shown that around 65% of people reuse passwords, so attackers will use automated attacks where they attempt to log in to applications using these stolen credentials. This is called credential stuffing. At Auth0, about half of the logins on our platform are bots attempting to carry out a credential stuffing attack.
To protect against this, Auth0 maintains a database of over 1 billion breached credentials so that any time one of your users who has been affected by a breach signs in to your application, they'll be notified that their password has been compromised by a different data breach. This can be turned on or off with a single click from your Auth0 dashboard.
Resources and Practice
For a lot of developers, myself included, it's easier to learn by doing. If you're interested in expanding your knowledge on different security topics, here are some awesome resources where you can test out everything you just learned:
- Contra Application Security — An interaction reconstruction of the Capital One breach
- RedTiger's Hackit — Practice SQL injections
- XSS Game — Practice XSS attacks
- bWAPP — An intentionally vulnerable application that you can download and play with
- OWASP — Goldmine of security resources
Hopefully, this guide has shown you have important web security is and helped you to understand how you can improve your own applications. Be sure to let me know if you have any questions and if you'd like to see a follow-up!