Reflected XSS/Open redirect not detected in React application

Product used:
SonarQube EE 9.6.0.59041

Rules affected: jssecurity:S5696, jssecurity:S5146, jssecurity:S6105

There’s an open redirect vulnerability in the UrlParam parameter, with javascript: URLs it can be escalated to an XSS as well. e.g. http://localhost/vulnerable?UrlParam=javascript:prompt(123)

The source is a GET parameter, the sink is the navigate function in @reach/router final sink in the lib source

The relevant code sections from the application:

import React, { useCallback } from 'react';
import { navigate } from '@reach/router';

const NavigateLink = ({ Content }) => {

	const onLinkClick = useCallback((e, url) => {
		e.preventDefault();
		const searchParams = new URLSearchParams(window.location.search);
		const referrer = searchParams.get('UrlParam');
		navigate(referrer || url || '/');
	}, []);
...

   return (
    <TextLink onClick={(e) => onLinkClick(e, url)}>{linkLabel}</TextLink>
    );
...
export default RouteContent('/vulnerable')(NavigateLink);

Let me know if further details are needed to reproduce the finding.

Hello @Adam_B,

Thanks a lot for this definitely complete work. We’ve been actually supporting React 9.6 for quite a short amount of time now, so it’s going to be one of our first feedbacks on this matter. Thanks a lot!

I see two actions on our side:

  • Supporting the navigate sink
  • Supporting Open Redirect escalations to XSS (this might need a new rule, for clarity’s sake)

Have a good day!

Loris

A PoC app (without our internal dependencies) for better reproducibility:
index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { useCallback, useState } from 'react';
import { navigate } from '@reach/router';

const useToggle = (initialState = false) => {
  // Initialize the state
  const [state, setState] = useState(initialState);
  
  // This function change the boolean value to it's opposite value
  const toggle = useCallback(() => {
      setState(state => !state);
      const searchParams = new URLSearchParams(window.location.search);
  const referrer = searchParams.get('UrlParam');
  console.log("URL: "+referrer);
      navigate(referrer || '/');
  }, []);
  
  return [state, toggle]
}

function App() {
  const [isDisabled, setIsDisabled] = useToggle();
  
  return (
      <button onClick={setIsDisabled} disabled={isDisabled ? 'disabled' : ''}>Click to navigate to URL</button>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
  <h1>Open redirect/XSS PoC</h1>
  <p>There's no validation of target URLs in @reach/router's navigate function. This function should be identified by SAST tools as a sensitive sink when supplying user input to it.
  <br/><br/>Open the links below then click on the button to navigate. The injection point is in the <i>UrlParam</i> GET parameter</p><br/>
  <a href="?UrlParam=https://example.com">Open redirect (example.com)</a><br/>
  <a href="?UrlParam=javascript:confirm('XSS')">Cross-site scripting (confirm dialog)</a><br/><br/>
    <App />
  </React.StrictMode>
);
1 Like

Thanks for this. I will use this in our internal tickets.

Hey,

For the record, I created two tickets to track the support of navigate and the escalation from Open Redirectts to XSS (unfortunately closed-source):

Have a good day!

Cheers,

Loris

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.