DEV Community

Petra Grunheidt
Petra Grunheidt

Posted on

Obfuscating your create react app and routes

This week, I was tasked with enhancing the security of our Create React App by employing obfuscation and minification techniques.

Obfuscation, as the name implies, obscures the source code, intentionally making it complex for humans to decipher while retaining its functionality. Minification, on the flip side, compresses code, trims the fat, and optimizes size for efficient delivery.

During my intial search i came across some outdated libraries like javascript-obfuscator and uglify-js(as if javascript code can get any uglier, am I right?). Then, I stumbled upon Terser, a modern library that supports ES6.

Reading through the documentation I was able to apply a simple enough script for my package.json:

"obfuscate:" "terser build/static/js/main.*.js --compress --mangle --output main.min.js"
Enter fullscreen mode Exit fullscreen mode

Which I then tinkered with using xargs for it to update the same file that was used instead of generating a new one:

"obfuscate": "find build/static/js -type f -name 'main.*.js' -print0 | xargs -0 -I {} terser {} --compress --mangle --output {}",
Enter fullscreen mode Exit fullscreen mode

I also came across an article recommending setting the GENERATE_SOURCEMAP to false. A source map is a file that maps the minified or transpiled code back to its original source code, aiding in debugging. While source maps may be valuable during development for debugging purposes, they can potentially expose sensitive information about your code structure and logic. So my final script looks like this:

"build": "GENERATE_SOURCEMAP=false react-scripts build && yarn obfuscate",
Enter fullscreen mode Exit fullscreen mode

Now when I open the website, the files are obfuscated and minified:

Obfuscated build
(Apologies for the light mode, i don't usually use the browser i printed this on. This is not an obfuscation technique, as it is local to my browser)

So, job well done, right?

However, a colleague conducting security checks on the application discovered that it remained remarkably easy to discern all the routes by searching for the path. This is understandable since the route names can't be effectively obfuscated, as the pathnames must be preserved for users to be able to access the routes. While this is acceptable for public routes, every file in the application is somehow present in the /build/static/js/main.*.js file, given that it's a JavaScript SPA.

The issue arose because our app contained routes meant to be 'secret' and hidden from regular users. Although these routes were protected by authentication, exposing them wasn't an ideal scenario

Chunkification

Now, here comes the really interesting part of this article. The key to obfuscating routes lies in leveraging the power of React's lazy loading and suspense mechanisms.

  • Lazy Loading: Lazy loading allows you to load specific parts of your application only when they are needed. This is particularly beneficial for large applications with numerous components. React's lazy function enables you to dynamically import components, ensuring that they are only fetched and rendered when required. This helps in reducing the initial loading time and improving the overall performance of your application.

  • Suspense: Suspense is a React feature that allows components to wait for something before rendering. It enables you to manage the loading states of your components more efficiently. With the Suspense component, you can specify fallback content to display while waiting for the main content to load. This creates a smoother and more seamless user experience, especially when dealing with asynchronous operations such as lazy loading.

By strategically employing these features, we can dynamically split and load code associated with specific routes into separate chunks within your build/static folder, and it will only be loaded when the route is accessed. This not only enhances the security of your application but also optimizes the loading of resources, providing a more efficient and seamless user experience.

When applying this technique and building the app, our Chunk will appear like this:

Chunky the Chunk

Now, let's go to the implementation

Implementation in Routing

In your main routing configuration (app/index.js), you can structure it as follows:

// app/index.js
<Router>
  <Routes>
    <Route path="/*" element={<IndexRoutes />} /> {/* Regular routes */}
    <Route path="/admin/*" element={<Suspense fallback={<></>}><AdminRoutes /></Suspense>} /> {/* Routes you want to hide */}
  </Routes>
</Router>
Enter fullscreen mode Exit fullscreen mode

The AdminRoutes component, which contains the routes you wish to secure (routes/admin.jsx), could look something like this:

// routes/admin.jsx
<Routes>
  <Route path="/secret-clients/:slug" element={<AdministrativePage />} />
  <Route path="/super-secret-page" element={<SuperSecretPage />} />
  {/* ... */}
</Routes>
Enter fullscreen mode Exit fullscreen mode

This approach allows the existence of the /admin namespace to be evident in your main.*.js file. However, the actual code related to this namespace is encapsulated in a separate chunk that won't load until the corresponding routes are accessed.

There is just one potential issue: While our admin chunk is inaccessible from regular routes, accessing host/admin/{anything} would load a page. The page would be blank, but unfortunately for us, this also triggers the loading of the associated chunk, potentially exposing information about the routes.

To add an extra layer of protection against unauthorized access to our chunk, we can implement a redirect strategy within our routes/admin.jsx file:

  // routes/admin.jsx
  <Routes>
    <Route path="/actions/:slug" element={<AdministrativePage />} />
    <Route path="/super-secret-page" element={<SuperSecretPage />} />
    {/* ... */}
    <Route path="*" element={<Navigate to="/" />} />
  </Routes>
Enter fullscreen mode Exit fullscreen mode

By including <Route path="*" element={<Navigate to="/" />} />, we ensure that any attempt to access our chunk with an unknown path will result in an immediate redirect to the homepage. While this provides improved security, there's still a brief moment when the chunk is accessible before the redirect occurs, and you know hackers these days, with their black hoodies and big brains. If they can download cars, they could certainly download our chunk if they tried hard enough.

To further enhance security, we can integrate this strategy with user authentication. Returning to our app/index.js, we can modify the routing configuration:

// app/index.js
import { isAdminLoggedIn } from './dir' /* function to verify authentication*/

{...}

<Router>
  <Routes>
    <Route path="/*" element={<IndexRoutes />} /> {/* Regular routes */}
    <Route path="/auth/*" element={<IndexRoutes />} /> {/* Auth routes */}
    {isAdminLoggedIn() && <Route path="/admin/*" element={<Suspense fallback={<></>}><AdminRoutes /></Suspense>} />} {/* Routes you want to hide */}
  </Routes>
</Router>
Enter fullscreen mode Exit fullscreen mode

By implementing authentication in combination to the chunk strategy, even if an unauthenticated user were to try and access the /admin namespace, it would not trigger the chunk loading, therefore, protecting our app further.

Top comments (7)

Collapse
 
jackmellis profile image
Info Comment hidden by post author - thread only accessible via permalink
Jack

This is really interesting but it's a whole lot of effort when obfuscation is the lowest form of security. Any decent hacker who wants to delve into your source code would not be put off by minified code - a mild inconvenience at best.

It's much safer and easier to just ensure your UI code doesn't contain sensitive information in the first place.

Collapse
 
petragrunheidt profile image
Petra Grunheidt • Edited

There are actually two points in this post, one about the obfuscation/minification. I agree that this part is a minor inconvenience.

The second part (splitting the build code into chunks) i find the most important/interesting in the context of a single page application, since, before this implementation, it was possible to access the source code of authenticated routes from the main.*.js

Collapse
 
j4k0xb profile image
j4k0xb • Edited

Code splitting doesn't improve security and the source code is still available once you load it or find the chunk id

Collapse
 
garoazinha profile image
Mariana Souza

another BANGER

Collapse
 
aaronucsd77 profile image
aaronucsd • Edited

Is there a way to not have this script (yarn obfuscate) build when on the development env? Like locally or stage? Non-production.

Collapse
 
petragrunheidt profile image
Petra Grunheidt • Edited

Hey aaron!

Yeah you can totally do that, locally when using a simple yarn run this script wont run, since build it not requried for local development/testing.
If you have a deployed staging environment, I would recommend obfuscating it, since it is public on the web, but if you would still like to skip obfuscating, you could just create two different build scripts on your package.json. One that runs yarn obfuscate and done that doesn't

Hope this helps!

Collapse
 
aaronucsd77 profile image
aaronucsd

Thanks for the reply. I actually end up adding two scripts as you stated. One build and another dev-build.

Some comments have been hidden by the post's author - find out more