This content originally appeared on JavaScript January and was authored by Emily Freeman
Hey! I'm Ryan and I am very fond of application security. Say hi on Twitter and check out my newsletter for more auth and security content.
Front end app security is a topic that probably doesn't get as much attention as it should. The really sensitive bits of your application might be a backend concern but there are still plenty of potential vulnerabilites on the front end as well.
React is very much unopinionated about how you build your app. Do it whichever way feels right! In the same way, React is not very prescriptive about how you might go about making sure that app is secure.
Let's look at three things you can do (relatively easily) in your React applications to boost your security posture.
1. Be Lazy: Only Load Stuff if the User Needs It
You may have seen Kent C. Dodds' post about splitting up your app into two distinct parts: one that is meant for authenticated users and the other for unauthenticated users. This is a huge win for security. Taking it one step further, we should also be hiding away smaller parts of the application that even some authenticated users shouldn't be able to see. For instance, we may have parts of the app that are only meant for "admin" users. Anyone without that role shouldn't be shipped the code for the admin areas. We can make this happen with lazy loading.
What is Lazy Loading?
By default, most single page applications will ship their entire code bundle to the user's browser when the page is first loaded. This make sense; the user needs all that JavaScript on the page if they are going to make use of the app. But this is overdoing it. What if the user only uses one or two of the routes in the app? Do they really need the code that powers the lesser-used routes that they might never actually see?
This is where lazy loading comes in handy. With lazy loading, we can define the point at which certain portions of the code for our SPA is loaded. This means you might only load the code responsible for displaying the default view (perhaps some kind of dashboard) when the user first loads the app, then when they navigate to another route, it's only then that they receive the code needed for that route.
If code splitting is properly set up in your application, this is fairly trivial. You can tell React to lazily load a portion of code by using React.lazy
.
Lazy Load Based on Role
Users who are unauthenicated shouldn't receive any code in the browser that is meant for authenticated users. In the same way, code that is meant for more users with a higher level of access shouldn't be shipped to those without those access levels.
We can make this distinction in a few different places. One is throughout our apps at the individual component level. Another would be at the route boundary level. We'll look at the former in this example.
We've got two simple components, AdminMessage
and UserMessage
. As you might guess, one is meant only for admins and the other for regular users.
// AdminMessage.js import React from 'react'; const AdminMessage = () => <h1>Admin Message</h1>; export default AdminMessage;
// UserMessage.js import React from 'react'; const UserMessage = () => <h1>User Message</h1>; export default UserMessage;
Let's set up lazy loading for these so that only the relevant one (depending on the user's role) gets loaded in the browser.
Note: we're using a simple hard-coded
role
value for the user in this example. In practice, this info would come from the user's profile or something like anid_token
// App.js import React, { Suspense, useState } from 'react'; const UserMessage = React.lazy(() => import('./UserMessage')); const AdminMessage = React.lazy(() => import('./AdminMessage')); const App = () => { const [user] = useState({ name: 'John Doe', role: 'user' }); return ( <div> <Suspense fallback={<div>Loading</div>}> {user.role === 'admin' ? <AdminMessage /> : <UserMessage />} </Suspense> </div> ); }; export default App;
Instead of importing components as we might usually do, here's we're using React.lazy
to defer shipping the code for those components to the browser until they are actually needed. But when is this code actually needed? In this case, that decision is being made simply by reading the user's role
when the main App
component loads.
If you load this up in the browser and inspect the network requests, you'd find that only the code relevant to the UserMessage
component gets loaded. Nothing at all for the AdminComponent
.
A great use case for this is when you have one application that is used by parties both inside and outside of an organization. You wouldn't ever expose an API endpoint that would allow external users to perform an internal-user-only action. But to be even more secure, you should prevent the external users from even having access to the internal-only front end code.
2. Don't Send Tokens to Where They Don't Belong
Most React apps get their data from some sort of JSON API. This might be your own API that you control, but it could also be a third-party API that you have no control over whatsoever.
If you happen to be using JSON Web Tokens to authenticate requests that go from your app to your API, chances are you're sending them in the Authorization
header of the HTTP requests. If you're using axios to make these requests, perhaps you're making use of its interceptor feature which allows you to modify all outgoing requests such that they contain the JWT in the Authorization
header.
If you're doing this, it's really a good idea to make sure you're not sending that JWT to third-party APIs that you don't own. Who knows what that server might be doing with your tokens! They're mostly likely not doing anything nefarious, but they're probably at least logging the request info. In any case, it's a really good idea to make sure you only send tokens to where they should go.
Set Up Axios Interceptors With an Allowed List
We can set up our axios interceptors to only send tokens to places we've deemed ok. To do this, keep an array of allowedOrigins
in the interceptor configuration and consult it before attaching the token in the request.
axios.interceptors.request.use(config => { const { origin } = new URL(config.url); const allowedOrigins = ['https://api.mysite.com']; // if the origin of the request is in the // allowed list, attach the access token if (allowedOrigins.includes(origin)) { config.headers.Authorization = getAccessToken(); // some function to get the token } return config; })
3. Be Careful with dangerouslySetInnerHTML
As a general rule, you should avoid setting the content of a DOM element using innerHTML
whenever possible. Doing so exposes your application to potential XSS (cross-site scripting) vulnerabilities. One common scenario when the content of an element needs to be set using innerHTML
is when you have some content that is pre-written using HTML itself, for example, content from a blog post. You want to be able to display all the formatting that comes with that HTML content, and to do so, you need to tell your React app to render it as is.
While useful, this can be dangerous because if someone is able to slip a <script>
tag into that HTML content, you're going to be left with potential XSS attacks. This includes everything from stealing tokens that might be somewhere unsafe (like local storage) to actually performing actions on the app's backend (if the attacker is crafty enough).
To let you know that you're doing something risky, React makes you type out the word "dangerously" if you really want to set the innerHTML
.
const BlogPost = ( ) => <div dangerouslySetInnerHTML={{ __html: html }}></div>
Some front end frameworks help us out when working with innerHTML
by automatically sanitizing the content that goes into it. Angular is one such framework that does this.
With React, however, if we want to sanitize the input, that's left up to us as developers. And we definitely do want to sanitize the input.
There are a few different packages for sanitizing HTML, but I highly recommend dompurify. It has been around for a long time and is widely used. It's also pretty simple to bring into your React app and make use of right away.
import React from 'react'; import DOMPurify from 'dompurify'; const BlogPost = ({ html }) => ( <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}></div> );
Calling DOMPurify.sanitize
on our HTML content, we can be assured that any kind of potentially malicious HTML will be stripped away. For example, if our HTML contained a <script>
tag, DOMPurify would take care of removing it altogether.
To get an idea of what else DOMPurifiy sanitizes for us, have a look at the readme.
Wrapping Up
There's a lot to think about when it comes to securing your React applications. The considerations above are just a few of the ways you can beef up your security posture. These examples can give you a couple quick wins, but I highly recommend going beyond this and making sure your app is locked down properly.
I've got some more React security content in the works. Follow me on Twitter for tips and tricks and subcribe to my newsletter for more info. I'll have some free video resources about React security coming out shortly!
This content originally appeared on JavaScript January and was authored by Emily Freeman
Emily Freeman | Sciencx (2020-01-20T14:25:00+00:00) Three Things You Can Do To Make Your React App More Secure. Retrieved from https://www.scien.cx/2020/01/20/three-things-you-can-do-to-make-your-react-app-more-secure/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.