Are you in doubt that your React routes are really protected ? There are lots of tutorials out there that talk about “secured routes”, “protected routes”, “private routes” or “restricted routes”. It’s pretty misleading because even though you have protected routes, it is possible to get past your login page and get access to all the code in your protected routes.
Perhaps people are telling you that it isn’t worth going for server side rendering (SSR) in order to truly protect the graphic content of your website. Maybe they’re saying that securing the backend should be enough because that will prevent you from displaying any sensitive data to fake-logged-in users. But WHAT IF you still want more security and you want to block all access? After all, you don’t want your competitors to hack into your admin dashboard, to see how you’re progressing or stealing your ideas.
This article will answer the following questions
- what's not secure about protecting routes in the client?
- why do we protect routes in the client?
- in which case do I want to have truly protected routes?
- how to truly protect routes
What's not secure about protecting routes in the client?
In React there is nothing such as truly private routes as it's a single page application (SPA) which means that all the code is served to the browser.
This is typically how we protect routes in the browser:
{isLoggedIn ? <PrivateRoutes> : <PublicRoutes/>}
With this code here above, you can't guarantee that the user won't tweak your javascript, change isLoggedIn
to value true and pretend to be an authenticated user.
Let's see how we can get access to ALL the code of your application. Here's a sandbox example where I've made a typical login system with protected routes. Notice that I lazy loaded two components: "PrivatePage" and the "LoginPage". Then I used the "classic" way of importing "AnotherPrivatePage" even though that component is not being used (this is on purpose).
import React, { useState, Suspense } from "react"
import AnotherPrivatePage from "./AnotherPrivatePage"
const PrivatePage = React.lazy(() => import("./PrivatePage"))
const LoginPage = React.lazy(() => import("./LoginPage"))
export default function App() {
const [isAuthenticated, setAuthenticated] = useState(false)
return (
<Suspense fallback={<div>Loading .. </div>}>
{isAuthenticated ? <PrivatePage /> : <LoginPage />}
</Suspense>
)
}
You can either follow the article or test yourself by opening up the sandbox example, and opening page in new window by clicking on the two squares in the upper right corner (the icon can vary between browsers):
Go to devtools by right clicking, choose "Inspect" (if you're in Chrome). Then go to "Sources".
Here above you can see that we have two components loaded to the browser, "LoginPage" because isAuthenticated = false. We also have "AnotherPrivatePage" because if you don't lazy load, we can very easily access that component as well. The "hacker" doesn't even have to hack to look around and read the code and maybe see some static data.
It needs a bit more effort to get hold of the other component "PrivatePage.js" as it's lazy loaded. There are lots of ways to do that, but here's one:
Install React dev tools if you don't have it already, go to ⚛️Components:
Then click on "App" and change hook's state to true:
And you'll see how we get access to the "PrivatePage", the last component we didn't have loaded in of our application and was supposed to be protected. There are of course lots of other ways to hack React. To increase security you could for example disable access to devtools in production but there's most often some other way to get around things.
But why do we then protect our routes in the front end?
You can protect your components/graphics on a:
component level
route level
Either way, the main reason for why we're protecting those graphics is just to make the user experience nicer. The reason why we do it on a route level is just to make our code more organized by avoiding duplications.
How are protected routes nicer for the user ? Imagine, the user has already visited our page. Next time he visits, he'll tap the url of your website and his browser autocompletes the website url without adding /login to the end of the URL. He goes straight to http://www.somewebsite.com, but he's not authenticated anymore (let's say that he logged out the last time or his authorization token has expired). And because he's not logged in anymore the user will see the page without any content and no possibility to interact with anything that has to do with server data. It would be nicer for the user to have no direct access to the private pages and instead automatically land on the login page.
But is it so important to have truly protected routes?
In the worst case scenario, the user can hack its way with javscript to your private routes and will see some empty tables, graphs, or messages that tell you that there is no data etc. And without content, your website will look like nothing, might even be ugly or at least it will be unusable. Well that's not so serious, we could even say that our hacker deserves that! 😈. But you have to make sure that there is no possibility for the hacker to access sensitive data 🔓 You should not leave any sensitive static data in your client and ensure that all your API endpoints are secure and make the server throw 401 if the user is not really authenticated and authorized.
But is that really enough? Like I said above you might have built an admin dashboard for your company. Even without access to sensitive data, your competitor could possibly deduce where your company is heading by reading any static texts in your app, or by trying to make sense of your graphics, even though they're missing the content. Apart from that, truly securing the private part of your app adds an extra layer of security to your app, which can only be positive.
How to make truly secured routes?
There are several ways to achieve this. You could use SSR to solve this problem or you could stay with 100% SPA and serve your application in two parts. I've an example of the how to achieve the latter solution. There are lots of ways to do this and here I have an example of this using Express server in Node.js that serves two different SPAs, one containing the login page and the other containing the app itself. You can see this project here on github.
If you clone that project and run it, you should be aware that it takes pretty much time. Instead you can also just follow the article and check out the code.
If you run the project and go to devtools, you'll see in "sources" that you only have the login page loaded to the browser.
Here there's no possibility to access the authenticated part of the application because it won't be served to the browser unless you provide the correct auth inputs in username and password thanks to this code in server.js
app.get("/protected", (req, res) => {
if (req.signedCookies.name === "admin") {
app.use(express.static(path.join(__dirname, `/${privatePage}/build`)))
res.sendFile(path.join(__dirname, `/${privatePage}/build/index.html`))
}
})
You can try to log in, username: admin
and password: 123
...
and voilà:
Here we're logged in and now we have the authenticated part of the application loaded in the browser and as a side effect, the login page is no more loaded in the browser.
I hope this article has been useful for boosting the security of some of your websites that might use some extra layer of restriction! If you found this article helpful, don't hesitate to leave a comment or share it with others. Same of course if you have something that you would like to point out :)
This post was originally publisehd on daggala.com, November 16, 2020
Top comments (10)
Nice write up. I'd add something though. I never return a 401 if the user unauthenticated. Return a 200, let the hacker think they succeeded, it slows them down a lot more than if you tell them they failed.
Better yet, return a fake object, or make your endpoints check for a specific response value and redirect them to the login screen forcing them to start over.
Personally for all hack attempts I capture I now return the message "invalid token", except the project I'm working on doesn't use a token. Can't wait to get support requests asking me where they can find their token as no actual user would ever encounter those messages without digging in the network tab.
Better yet, I've seen people propose that when your endpoint detects a bot, don't respond and don't time out, leave it hanging, waiting endlessly for a response that never comes.
If someone wants to hack you, make it hard on them, but also fun for you :)
Interesting! So in the case the front end relies on 401 (for example when fetching a refresh token) do you then hide some clues in the response so that the front end will know when to fetch another refresh token ?
In that case you could just add a value to the object like "message":"nogo" and use that to determine it, but since all failed responses return the same message it throws a potential hacker off the scent since there's nothing useful to it.
neat! thanks for the tips! :)
Interesting read! although we can hide all the sources(files, components, redux, etc)
by just adding a single line in our package.json file. This hides all the source maps when we make a production version of our application.
The line is:
"build": "GENERATE_SOURCEMAP=false react-scripts build"
Read about it here:
stackoverflow.com/questions/514157...
thanks for your reply! I definitely should have mentioned that in the article... and also that react dev tools could be disabled in production as well!
Just clarifying for possible future readers that even though you disable the sourcemap and react dev tools, you will still have access to the css files and javascript and you can find ways to do the same hack that is demonstrated in the blog
Hmmmm... I see. Thanks for the amazing article though!
You don't seem entirely convinced :) But if you go to the webpage of you react app, you go to the source of your index.html that you can see in chrome dev tools -> Network tab. You could scroll down to the script tags <script src="/static/js/main.b5b80c7f.chunk.js"> open it and you'll see that you have access to all the minified javascript chunk. The browser needs to run this javascript so sadly it is accessible to the user. It is however minified so it would be harder to do this hack in my article, but it's possible anyways. (I show here an example on localhost but this would also work on production)
It's not like I was not convinced.. 😂
I was curious about it and searched for more regarding this and found exactly what you've mentioned in this thread.
Kudos to you... 🔥🔥
I would've not paid attention anytime soon to this detail if it wouldn't have been your article.
hahah ^^ I see! lovely to hear, thank you :)