This is the second post in a series about web security for SPAs. In the last post, we laid the groundwork for thinking about web security and applying security mechanisms to our application stack. We covered the OWASP Top Ten, using secure data communication with SSL/TLS, using security headers to help enhance built-in browser mechanisms, keeping dependencies updated, and safeguarding cookies.
Ready to continue and make your SPA safer? This post will use the concepts we introduced to banish some well-known web vulnerabilities.
Practice data cleanliness to mitigate XSS
Cross-Site Scripting (XSS) is a vulnerability that continues to plague web developers. This vulnerability is a type of injection attack and is so common that it's number 3 on the OWASP Top Ten list.
How does this vulnerability work? In short, XSS happens when code pollutes data, and you don't implement safeguards.
Ok, fine, but what does that mean? A classic example is a website that allows user input, like adding comments or reviews. Suppose the input form accepts anything the user types in and doesn't appropriately safeguard that user input before storing and displaying it to other users. In that case, it's at risk for XSS.
Imagine an overly dramatic but otherwise innocent scenario like this:
- A website allows you to add comments about your favorite K-Drama.
- An agitator adds the comment
<script>alert('Crash Landing on You stinks!');</script>
.- That terrible comment saves as is to the database.
- A K-Drama fan opens the website.
- The terrible comment is added to the website, appending the
<script></script>
tag to the DOM.- The K-Drama fan is outraged by the JavaScript alert saying their favorite K-Drama stinks.
So, this is obviously awful. Least of all because we all know "Crash Landing on You" is, in fact, wonderful. Also, this scenario exposes your website to the possibility of a colossal breach in which an injection attack can do all sorts of bad things. With JavaScript, the attack could grab cookies, dig around your browser's local storage for authentication information, access your file system, make calls to other sites, and do even more harm.
In this case, we've show an example very clearly using a <script></script>
tag to make it easier for us to talk about, but an attack can use anything that runs JavaScript, such as adding an HTML element with JS embedded in the attribute, adding JS to resource URLs, embedding JS into CSS, and so forth. This is the true stuff of nightmares!
How do we avoid these sorts of shenanigans? There are a few different defensive tactics, but the main one is to ensure you practice good data hygiene through escaping and sanitizing.
Escaping refers to replacing certain specific characters in HTML so they are read as text strings, such as replacing <b>
with <b>
). When you escape, the browser no longer treats the value as part of the code. It's simply text or data. Although we're talking XSS here, escaping is a good practice in general as it will protect you against SQL injection, a different type of injection attack.
-- xkcd
Sanitizing removes code that might be malicious, while preserving some safe HTML tags. The primary use case for choosing to sanitize instead of to escape the code is when you want to allow markup to display, like if you have a rich text editor on your website.
There's a lot more to discuss regarding cross-site scripting, including types of XSS and mitigation technique specifics, so keep an eye out for a follow-up post with more details, including the built-in XSS security mechanisms provided in some SPA frameworks.
Dive into XSS
Can't wait to read more about XSS? Check out these resources that cover how cross-site scripting works:
- API Security book chapter on Sanitizing Data by Okta
- Cross-site scripting resource from Port Swigger Web Security Academy
Validate requests for authenticity to mitigate CSRF
Cross-Site Request Forgery (CSRF) is another well-known vulnerability in the top spot of the OWASP Top Ten, Broken Access Control. CSRF allows attackers to exploit your identity to perform unauthorized actions. Sounds pretty bad, right? Well, yup, you're correct.
The exploit works like this.
- You log in to the K-Drama site to upvote some recent shows you watched.
- Then, in a loss of judgment, you open a spam email and click the link to transfer money to an agitator claiming to be your long lost high school sweetheart. (What were you thinking!)
- The link takes you to a malicious site with a hidden form embedded within it.
- The hidden form sends an HTTP POST request with terrible comments to the K-Drama site along with the active session cookie.
- Since you have an active authenticated session on the K-Drama site, the POST completes as if you were the one adding terrible comments about "Hospital Playlist"!
Luckily in this example, the unauthorized action isn't irreversible or devastating. You can delete the terrible comments, and you can explain how it wasn't really you who posted such comments to horrified "Hospital Playlist" fans. However, you can see how dangerous and malicious this exploit would be if the target was something fundamental like a bank!
The only way to mitigate against CSRF is to ensure the request is legitimate.
Fortunately, we already have some tools and actions at our disposal that we covered previously, such as:
- Prefer tokens in HTTP headers for authenticated HTTP calls and ideally store those tokens in-memory.
- Configure tight CORS controls.
- Protect your cookies!
These mechanisms are so good that some say there's nothing more you need to do for SPAs!
However, CSRF relies on your backend not completing due diligence to verify the authenticity of the requests and allowing non-JSON payloads. Hackers are very clever, and you may need to allow <form>
payloads in your backend, so you may need extra safeguards. When you do need extra guards, you can add a unique CSRF token for communication between your frontend and backend. The backend needs to verify standard authentication and authorization safeguards and verify the CSRF token's legitimacy.
The token is generated by the backend and sent to the frontend. The frontend then uses the token in subsequent calls to the backend by adding it as a custom HTTP header.
For SPAs, getting that CSRF token from the server is the difficult part. In traditional web apps, it's not a problem. But for SPAs, you don't call your back-end API until after you load the application. You also want CSRF protection before logging in if you're not delegating authentication to a third-party authentication provider. 😎
The recommendation is to have your client call an endpoint on your backend to get the CSRF token. The endpoint must be super vigilant about confirming the caller's origin and keeping that CORS allowlist very strict.
Dive deeper into CSRF
There are a lot of browser protections that help us mitigate CSRF. Check out these links if you want to learn more about CSRF.
- Prevent Cross-Site Request Forgery (CSRF) Attacks by Auth0
- Cross-Site Request Forgery Prevention Cheat Sheet by OWASP
- Understanding CSRF from the Express team
Learn more about common web attacks
Stay tuned for the next post in this series as we dive deeper into CSRF and learn how Angular helps protect against it.
Ready to learn more? Check out out the following resources.
- A Comparison of Cookies and Tokens for Secure Authentication
- Defend Your Web Apps from Cross-Site Scripting (XSS)
- 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.