There's a lot of information floating out there about web security. But when I read through the material, I noticed some information wasn't up to date, or it was written specifically for traditional server-rendered web applications, or the author recommended anti-patterns. In a series of posts, I will cover web security concerns that all web devs should be aware of, emphasizing client-side applications, namely Single Page Applications (SPAs). Furthermore, I'm not going to get into the nitty-gritty of web security, cryptography, and networking. Instead, I'll focus on the main takeaways and high-level to-do lists. I'll also provide links to resources where you can dive in deeper.
So why do we worry about web security anyway? When we have vulnerabilities in our applications, it's an avenue for exploitation by bad actors. In the wrong hands, exploiting the vulnerabilities can cause risks to your application, data, reputation, and bottom line. In the words of my coworker Aaron Parecki, it all boils down to one thing—liability. And we don't want to expose ourselves, our applications, and our companies to liability. If you feel threatened, it's a good time to read on and identify how to harden your application.
If you're new to thinking about security, you may not have heard of the Open Web Application Security Project (OWASP). OWASP is a group that focuses on making the web a safer place. They maintain and publish a list of the most common web vulnerabilities named the "OWASP Top Ten" and have a new top ten list for 2021; their infographic shows how the top web vulnerabilities have changed from 2017 to 2021.
As this is a list of web vulnerabilities, the entire list is pertinent to our work as web developers. However, a few critical vulnerabilities call for extra attention, and some vulnerabilities are less susceptible when the front-end is a SPA with a JSON-based backend API.
We will cover some of these vulnerabilities specifically, but I'll also list general things to keep in mind that feed into the vulnerabilities listed here.
First, we need to set the stage for part of our vulnerability mitigation strategy by bringing up security headers. Modern browsers (buh-bye Internet Explorer!!) have a lot of security mechanisms already built in. We have to leverage the built-in security mechanisms and can enhance them with additional security headers. Security headers are HTTP response headers that provide instructions on how the browser should behave. They are defined on your web server.
There are quite a few security headers that we can apply. Something to note, SPAs are unique, so not all security headers apply in the same way as they would for a traditional server-rendered web application. I'll call out the security headers that you'll want to use when we discuss how to mitigate a specific vulnerability.
Here are some great resources to learn more about security headers. Figuring out how to configure the security headers can be esoteric. I like these posts that are short and offer actionable steps to configure security headers.
- An Overview of Best Practices for Security Headers
- OWASP's HTTP Security Response Headers Cheat Sheet
- Web.dev's Security headers quick reference
Using HTTPS should be the undisputed standard, but there are legacy systems where applications don't use HTTPS. It's essential to use the secure hypertext transfer protocol; after all, the HTTPS acronym is appropriately derived from Hypertext Transfer Protocol Secure. And it's essential to be secure across your entire stack, not just when serving the front-end site (don't ask me how I know this can be a thing).
When you don't use an up-to-date Transport Layer Security within your application system, you expose yourself to meddler-in-the-middle attacks (MITM). In this type of attack, agitators intercept communication between unsecured systems to siphon data or impersonate communicating parties.
You might think this callout to use HTTPS is unnecessary because it's such a standard for hosting web apps, but cryptographic failures that cause insecure communication are the second most common vulnerability in the OWASP Top Ten! Note that it's not enough to slap any old mechanism for HTTPS on your web server. You want a recent version of TLS. Your cloud providers might even force you to upgrade if your TLS version is old, which is good!
What's the takeaway here? Specifically, you'll want to make sure you have functioning certificates, ensure you're using the latest version of TLS, and make sure you use SSL/TLS over your entire stack. All of these steps might already be covered by your cloud hosting provider. You should also verify you've added the HTTP Strict Transport Security (HSTS) security header to the web server, which adds an extra layer of security beyond HTTPS redirection.
Check out these resources if you want to read more about Transport Layer Security and how to set up HTTPS redirection and HTST.
- Okta's API Security book chapter on Transport Layer Security
- OWASP's Transport Layer Protection Cheat Sheet
- TLS Guidelines from MDN
- Getting started with HTTPS and certificates with Let's Encrypt
You knew this was coming, even though we might lament the work involved to stay up to date. The advice to update your dependencies sounds like "Don't forget to floss," but it's essential (just like flossing). Vulnerable and outdated components take the sixth spot in the OWASP Top Ten list. And there were some significant vulnerabilities from dependencies in the news recently. So you know deep down that burying your head in the sand is not a good action plan.
It's important to note that we should update outdated components and ensure we don't accidentally depend on a vulnerable dependency. We should pin dependency versions in our
package.json and then generate and enforce the use of a lock file. That means running
npm ci instead of
npm i to force the exact versions defined in the
package-lock.json. If you use Yarn, you will use
yarn install --frozen-lockfile. This way, updating versions is an intentional act.
That's it. Just do it. It's important. Outdated components are a source of liability.
Cross-origin resources are a particular concern for SPAs; this refers to resources your web app uses that are not hosted on the same origin as your web application. When all the resources the front-end needs are available through the same origin, we have confidence that the resources are secure. Hopefully, we're not pwning ourselves! At least not intentionally, right?!
A typical pattern for setting up URLs for SPA communicating to back-end APIs might look like the following, where your front-end is
and the back-end API URL is
But the API URL is a subdomain, so the request is cross-origin. Most browsers (once again BUH-BYE Internet Explorer!) protect us by allowing only specific cross-origin requests. We can configure resource access by enabling Cross-origin Resource Sharing (CORS).
Between your production environment setup resembling the above URLs, and local development using different ports for your front-end and back-end, you might be tempted to enable CORS to allow all sorts of requests. Unlike other security measures covered in this post, your browser already protects you in the strictest way possible. Enabling CORS loosens that restriction.
So the advice is to enable CORS appropriately with considered use of allowlists. Use proxies as appropriate (such as for local development). The concept of least privilege applies here too, and keeping a tight lid on things helps mitigate security vulnerabilities.
Want to understand better when CORS rules apply or how to set up the allowlist rules? Read these resources for a deep dive.
Really, this advice is applicable outside of web development too. But since we're talking about web development in this post, we'll skip mitigation strategies about protecting your physical cookies from marauders and focus again on security headers.
Cookies can contain all sorts of goodies, such as chocolate chips and the tidbits of data that agitators love to have! Current authentication best practices favor using in-browser memory or web workers for authentication tokens over cookies, and cookies should never be used for secrets, but there might still be sensitive information stored in cookies. To help protect cookies, we can use attributes.
Cookies should be temporary. In particular, cookies with sensitive information should be as short-lived as possible. When you or your back-end bakes up a cookie, adding the attribute
Max-Age defines the longevity of the cookie.
There are some more attributes you can bake in! Adding the
httpOnly attribute is an excellent first step to guarding your cookies against a malicious script that can read your cookies.
This is where the
SameSite attribute comes into play. You can control when cookies should be sent by using the
SameSite attribute and setting one of 3 values:
Strict- only send cookies if they are going to the same site that requested them.
Lax- only send cookies when the user is navigating to the origin site. (This is the default behavior now for Chromium-based browsers.)
None- send the cookies to everyone, everywhere. As a safeguard for your generous behavior, which is concerning to the browser, you're also required to add the
Be thoughtful about cookies and apply principles of least privilege. Safeguarding cookies is a big step in mitigating other vulnerabilities.
Learn more about protecting your cookies by checking out these fantastic resources.
Stay tuned for the next post in this series as we learn about two well-known web security attacks, along with mitigation techniques using what we covered here.
Can't wait to learn more? Check out out the following resources.
- A Beginner's Guide to Application Security
- How to Configure Better Web Site Security with Cloudflare and Netlify
- Security Patterns for Microservice Architectures
- Security and Web Development
- OWASP Top Ten 2021: Related Cheat Sheets
Don't forget to follow us on Twitter and subscribe to our YouTube channel for more great tutorials. We'd also love to hear from you! If you have any questions or want to share what tutorial you'd like to see next, please comment below.