React Open Redirect
Guide: Examples
and Prevention

stackhawk

StackHawk|September 25, 2021

In this guide, learn about React open redirect vulnerabilities and how you can prevent them in your React application.

React Open Redirect Guide: Examples and Prevention - Picture 1 image

From payments and password resets to downloads, website redirections are everywhere. They're a popular way to perform background actions and navigate users to the relevant pages after the action is complete. 

However, with a subtle blend of social engineering, attackers could use your website's redirection feature to steal your users' data. Open redirect is a vulnerability that allows an attacker to control your website redirections. 

But what exactly is open redirection, and how can you prevent it? 

In this post, you'll understand what open redirections are and how you can prevent them in your React application.

Redirections: What, Why, and Where

You might think, "If open redirection is harmful, why not just disable redirections from the website?" Well, website redirections aren't a security vulnerability in themselves. You can consider them a feature or a way to enhance user experience for some actions on your site. Therefore, to understand open redirection, let's have a quick refresher on redirections in general. 

Let's say you have an e-commerce website where your users can buy some items. So you have a simple cart page where the user sees what items they're purchasing. 

Now let's say you decide to integrate a third-party payment provider into your application. So now when the user lands on the cart page, you add a redirect URL property in your cart page's URL. 

React Open Redirect Guide: Examples and Prevention - Picture 2 image

Redirecting from cart to checkout page in an e-commerce site.

This allows you to grab the redirect URL and immediately redirect the user to the payments page, where they make the actual payment. This is a classic example of redirections in e-commerce websites. 

Other Examples

Websites that allow you to download files also use redirection. They land you on a Thank You page after your download is complete. Similarly, password reset links often contain a parameter in the URL for redirection. Once the user has reset her password, the redirect is to a Login or Home page. This provides a more engaging user experience and ensures that users don't drop off from your website after a process.

Open Redirection Vulnerability

Now you know why redirection is important and sometimes even necessary. Let's see what an open redirect vulnerability is and how an attacker can use it to cause some damage to your users. 

Let's say for any of the above reasons, site1.com allows you to redirect to another website. Consider the part of the URL that actually performs this action. The query parameter url tells you which website you should redirect the user to. 

React Open Redirect Guide: Examples and Prevention - Picture 3 image

Website Redirection.

Now imagine there's an attacker who wants to trick you into landing on her website. The attacker could convince you to open her website through social engineering and perform some phishing attacks on you. However, these kinds of phishing attacks are difficult to execute because users are mostly aware of the websites they're visiting. 

An open redirect vulnerability allows anyone to modify the redirect URL in the website externally. So instead of redirecting to the website you're supposed to go to, the attacker could modify the URL to another harmful website. 

Here's how such an attack might look:

React Open Redirect Guide: Examples and Prevention - Picture 4 image

Open redirect vulnerability.

Since the redirect URL could be externally modified, the attacker easily tricks you into visiting a malicious website. Because you came from an authentic website, you might not even notice it! After executing some harmful scripts on her website, the attacker could then redirect you back to the original website. Since redirections happen almost instantly, users think it's a normal flow for the website. 

That's what an open redirect is and how it can be damaging for your users.

React Open Redirect Example

Open redirect vulnerability can be exploited on both the client and server sides. For the sake of brevity, we'll be talking about the client-side vulnerability only in this post. If you want to learn about open redirects in server-side implementations, here's a great post that might help you out. 

Remember we talked earlier about redirections used in reset password pages? In this example, we'll create our own mock reset password page with a redirection. I'll then use it to demonstrate an open redirect attack. 

Setup React App

Let's create a new React app by running:

npx create-react-app react-open-redirect

We'll be using React-Router-DOM, so let's install it by running:

npm i react-router-dom

We're going to create a root route and a route that handles our Reset Password Page. Add the following code inside your App.js:

import './App.css';
import {
  BrowserRouter as Router,
  Switch,
  Route,

} from "react-router-dom";
import Homepage from './pages/Homepage';
import PasswordReset from './pages/PasswordReset';

function App() {
  
  return (
    <div className="App">
      <Router>

     <Switch>
          <Route path="/password_reset">
            <PasswordReset/>
          </Route>
          <Route path="/">
            <Homepage/>
          </Route>
        </Switch>
        </Router>
    </div>
  );
}

export default App;

The above code renders the Home page for all routes and renders the Password Reset page for the /password_reset route. Next, let's add these two pages.

Create a folder called pages inside the root directory. Add the file Homepage.js inside it with the following code:

export default function Homepage(){
    return(
        <div>
            <h1>Home </h1>
        </div>
    )
}

The above code simply renders an <h1> tag with the text Home. Next, let's create the Password Reset Page: 

import { useEffect, useState } from "react"
import { useLocation } from "react-router-dom"

export default function PasswordReset(){

    const {search}=useLocation()
    const queryParams=search.split('?').join().split('&')

    const [resetPassword,setResetPassword]=useState({
        oldPassword:'',
        newPassword:'',
        token:null,
        redirectTo:null
    })

    const getResetPasswordToken=()=>{
        const token=queryParams[0].split('=')[1]
        setResetPassword({...resetPassword,token})
    }

    const getRedirectURL=()=>{
        const redirectTo=queryParams[1].split('=')[1];
        setResetPassword({...resetPassword,redirectTo})
    }

    const handleChange=(e)=>{
        setResetPassword({
            ...resetPassword,
            [e.target.id]:e.target.value
        })
    }

    useEffect(()=>{
        getResetPasswordToken();
    },[])

    useEffect(()=>{ 
        if(resetPassword.token) getRedirectURL()
    },[resetPassword.token])

    const Reset=()=>{
        
    }

    return(
        <div className="password_reset">
            <h1>
               Hey there, reset your password!
            </h1>
            <div className="form-control">
                <label htmlFor="oldPassword">Old Password</label>
                <input onChange={handleChange} id="oldPassword" type="password" />
            </div>
            <div className="form-control">
                <label htmlFor="newPassword">New Password</label>
                <input onChange={handleChange} id="newPassword" type="password" />
            </div>
            <div className="form-control" style={{justifyContent:'center'}}>
                <button onClick={Reset}>Reset Password</button>
            </div>
        </div>
    )
}

The above code renders a Password Reset form with two input fields for old password and new password. It has a Submit button that fires a function when clicked. We also use the useLocation hook from react-router-dom to get the query parameters from the URL. When the page loads, we fire two functions that extract the token and a redirect URL from the query parameters. We then store them inside our state along with the new and old password that the user types in.

Finally, I also created some basic styles to make our Reset Password page look a bit better. You can add these inside the App.css file:

.App {
  text-align: center;
  min-height: 100vh;
  margin: 0;
  padding: 0;
  background: #8a2387; /* fallback for old browsers */
  background: -webkit-linear-gradient(
    to right,
    #f27121,
    #e94057,
    #8a2387
  ); /* Chrome 10-25, Safari 5.1-6 */
  background: linear-gradient(
    to right,
    #f27121,
    #e94057,
    #8a2387
  ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}

.password_reset {
  color: #fff;
  padding: 150px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-direction: column;
}

.form-control {
  display: flex;
  justify-content: space-between;
  width: 100%;
  max-width: 400px;
  margin: 10px 0;
}

button {
  background: #0f2027; /* fallback for old browsers */
  background: -webkit-linear-gradient(
    to right,
    #2c5364,
    #203a43,
    #0f2027
  ); /* Chrome 10-25, Safari 5.1-6 */
  background: linear-gradient(
    to right,
    #2c5364,
    #203a43,
    #0f2027
  ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
  outline: none;
  border: none;
  padding: 10px 20px;
  border-radius: 5px;
  color: #fff;
  cursor: pointer;
}

input {
  border: none;
  outline: none;
  padding: 5px 10px;
  border-radius: 2px;
}

Great, we're all set now! To open your React app, run npm start inside your root directory. If you now visit the URL http://localhost:3000/password_reset?token=123&next=home, you should see the following page:

React Open Redirect Guide: Examples and Prevention - Picture 5 image

Reset Password page.

Awesome! Next, let's see why this React app is vulnerable to an open redirect attack. 

Vulnerability

In the previous section, we created a Reset function that fires when the user presses the Submit button. Let's add the following code inside that function:

    const Reset=()=>{
        //Make an API call to reset password

        //Callback fired when Reset Password API gives a response
        setTimeout(()=>{
            window.location.assign(resetPassword.redirectTo);

        },0)
    }

In a real-life scenario, you'll make an HTTP request inside the Reset function. This request will send the old and new password that the user typed. It verifies the entries, and if everything looks good, it sends back a status code 200. At this point, you know the user's password has been successfully reset. 

However, notice that we have a redirectUrl property inside our resetPassword state. This redirectUrl is the URL you're supposed to redirect to after the user's password is successfully reset. 

Inside the Reset function, I can change the URL route to whatever value we have inside our redirecrUrl property. Since our current URL is http://localhost:3000/password_reset?token=123&next=home, we navigate the user to the Home page. You can press the Submit button to verify this behavior:

React Open Redirect Guide: Examples and Prevention - Picture 6 image

Redirected to Home page on Submit button press.

However, the problem with this approach is we have an open redirect vulnerability in our application. If you change the next property inside your URL to an external website like https://www.google.com and then press the Submit button, you'll be redirected to google.com. 

If this were a live application, an attacker could easily change the URL to point to a malicious website. What's more lethal is when that redirection happens, there's a chance of your reset password token being leaked to the attacker through headers of the HTTP request. If an attacker has this information, she can easily go and reset your password—and boom, there goes your account!

Prevent Open Redirect

Open redirects can be easily prevented by setting coding guidelines for your application. The most important one is to sanitize your redirect URLs. To put it differently, you should write code that doesn't let a user redirect to an external website. 

If you look at our current use case, we want the user to navigate to a different page of our own website only. This is a simple fix inside our Reset function. Instead of using window.location.assign, we can use the useHistory hook provided by react-router-dom instead. First, import it at the top inside PasswordReset.js.

import { useLocation, useHistory } from "react-router-dom"

To use the useHistory hook, we need to create an instance of it and store it in a variable: 

    const history=useHistory();

Almost there! Finally, our new Reset function now looks like this: 

    const Reset=()=>{
        //Make an API call to reset password

        //Callback fired when Reset Password API gives a response
        setTimeout(()=>{
            //window.location.assign(resetPassword.redirectTo);
            //window.location.href=resetPassword.redirectTo;
            history.push(resetPassword.redirectTo)

        },0)
    }

If you now visit the URL http://localhost:3000/password_reset?token=123&next=https://www.google.com and hit the Submit button, you'll be redirected to the Home page only. 

If you look at your Home page's URL, you'll see that it has the redirectUrl appended to it as a route parameter.

React Open Redirect Guide: Examples and Prevention - Picture 7 image

Open redirect fix using React router.

You can further remove this by using a function that changes the current URL without reloading the page. You need to fire this function every time your Home page component mounts on the DOM. 

Here's how your Home page.js file would look after accommodating the above changes:

import { useEffect } from "react";

export default function Homepage(){
    function changeurl() {
        var new_url = '/';
        window.history.pushState('data', '', new_url);
        
    }
    useEffect(()=>{
        changeurl();
    },[])
    return(
        <div>
            <h1>Home </h1>
        </div>
    )
}

If you want to enable external redirect websites, the safest thing to do is to have a list of websites you'll be redirected to. Then, before redirecting, you can check if the website is present in that list and redirect only if it's there. 

Add Automated Security Testing to Your Pipeline

Other Vulnerabilities

Attackers often combine open redirect vulnerability in combination with other vulnerabilities, such as XSS, CSRF, and so on, to cause more damage to your users. Using the above steps, you can ensure that you don't leave any redirects open to be exploited by attackers. I wrote a detailed guide on XSS attacks and how you can prevent them in your React application. If you're curious to know more, you can check it out here. You can also check out the rest of the StackHawk site, which includes a free signup and a searchable blog

This post was written by Siddhant Varma. Siddhant is a full stack JavaScript developer with expertise in frontend engineering. He’s worked with scaling multiple startups in India and has experience building products in the Ed-Tech and healthcare industries. Siddhant has a passion for teaching and a knack for writing. He's also taught programming to many graduates, helping them become better future developers.


StackHawk  |  September 25, 2021