Examine Firefox Inter-Process Communication using JavaScript in 2023

This is my update to the 2021 JavaScript IPC blog post from the Firefox Attack & Defense blog.

Firefox uses Inter-Process Communication (IPC) to implement privilege separation, which makes it an important cornerstone in our security architecture. A previous blog post focused on fuzzing the C++ side of IPC. This blog post will look at IPC in JavaScript, which is used in various parts of the user interface. First, we will briefly revisit the multi-process architecture and upcoming changes for Project Fission, Firefox’ implementation for Site Isolation. We will then move on to examine two different JavaScript patterns for IPC and explain how to invoke them. Using Firefox’s Developer Tools (DevTools), we will be able to debug the browser itself.

Once equipped with this knowledge, we will revisit a sandbox escape bug that was used in a 0day attack against Coinbase in 2019 and reported as CVE-2019-11708. This 0day-bug has found extensive coverage in blog posts and publicly available exploits. We believe the bug provides a great case study and the underlying techniques will help identify similar issues. Eventually, by finding more sandbox escapes you can help secure hundreds of millions of Firefox users as part of the Firefox Bug Bounty Program.

Multi-Process Architecture Now and Then

As of April 2021, Firefox uses one privileged process to launch other process types and coordinate activities. These types are web content processes, semi-privileged web content processes (for special websites like accounts.firefox.com or addons.mozilla.org) and four kinds of utility processes for web extensions, GPU operations, networking or media decoding. Here, we will focus on the communication between the main process (also called "parent") and a multitude of web processes (or "content" processes).

Firefox is shifting towards a new security architecture to achieve Site Isolation, which moves from a “process per tab” to a “process per site” architecture.

The parent process acts as a broker and trusted user interface host. Some features, like our settings page at about:preferences are essentially web pages (using HTML and JavaScript) that are hosted in the parent process. Additionally, various control features like modal dialogs, form auto-fill or native user interface pieces (e.g., the <select> element) are also implemented in the parent process. This level of privilege separation also requires receiving messages from content processes.

Let's look at JSActors and MessageManager, the two most common patterns for using inter-process communication (IPC) from JavaScript:

JSActors

Using a JSActor is the preferred method for JS code to communicate between processes. JSActors always come in pairs - with one implementation living in the child process and the counterpart in the parent. There is a separate parent instance for every pair in order to closely and consistently associate a message with either a specific content window (JSWindowActors), or child process (JSProcessActors).

Since all JSActors are lazy-loaded we suggest to exercise the implemented functionality at least once, to ensure they are all present and allow for a smooth test and debug experience.

Inter-Process Communication building on top of JSActors and implemented as FooParent and FooChild

The example diagram above shows a pair of JSActors called FooParent and FooChild. Messages sent by invoking FooChild will only be received by a FooParent. The child instance can send a one-off message with sendAsyncMesage("someMessage", value). If it needs a response (wrapped in a Promise), it can send a query with sendQuery("someMessage", value).

The parent instance must implement a receiveMessage(msg) function to handle all incoming messages. Note that the messages are namespace-tied between a specific actor, so a FooChild could send a message called Bar:DoThing but will never be able to reach a BarParent. Here is some example code (permalink, revision from March 25th) which illustrates how a message is handled in the parent process.

Code sample for a receiveMessage function in a JSActor

As illustrated, the PromptParent has a receiveMessage handler (line 127) and is passing the message data to additional functions that will decide where and how to open a prompt from the parent process. Message handlers like this and its callees are a source of untrusted data flowing into the parent process and provide logical entry points for in-depth audits

Message Managers

Prior to the architecture change in Project Fission, most parent-child IPC occurred through the MessageManagers system. There were multiple message managers, including the per-process message manager and the content frame message manager, which was loaded per-tab.

Under this system, JS in both processes would register message listeners using the addMessageListener methods and would send messages with sendAsyncMessage, that have a name and the actual content. To help track messages throughout the code-base their names are usually prefixed with the components they are used in (e.g., SessionStore:restoreHistoryComplete).

Unlike JSActors, Message Managers need verbose initialization with addMessageListener and are not tied together. This means that messages are available for all classes that listen on the same message name and can be spread out through the code base.

Inter-Process Communication using MessageManager Inter-Process Communication using MessageManager

As of late April 2021, our AddonsManager - the code that handles the installation of WebExtensions into Firefox - is using MessageManager APIs:

Code sample for a receiveMessage function using the MessageManger API Code sample for a receiveMessage function using the MessageManger API

The code (permalink to exact revision) for setting a MessageManager looks very similar to the setup of a JSActor with the difference that messaging can be used synchronously, as indicated by the sendSyncMessage call in the child process. Except for the lack of lazy-loading, you can assume the same security considerations: Just like with JSActors above, the receiveMessage function is where the untrusted information flows from the child into the parent process and should therefore be the focus of additional scrutiny.

Finally, if you want to inspect MessageManager traffic live, you can use our logging framework and run Firefox with the environment variable MOZ_LOG set to MessageManager:5. This will log the received messages for all processes to the shell and give you a better understanding of what’s being sent and when.

Inspecting, Debugging, and Simulating JavaScript IPC

Naturally, source auditing a receiveMessage handler is best paired with testing. So let's discuss how we invoke these functions in the child process and attach a JavaScript debugger to the parent process. This allows us to simulate a scenario where we have already full control over the child process. For this, we recommend you download and test against Firefox Nightly to ensure you're testing the latest code - it will also give you the benefit of being in sync with codesearch for the latest revisions at https://searchfox.org. For best experience, we recommend you download Firefox Nightly right now and follow this part of the blog post step by step.

DevTools Setup - Parent Process

First, set up your Firefox Nightly to enable browser debugging. Note that the instructions for how to enable browser debugging can change over time, so it's best you cross-check with the instructions for Debugging the browser on MDN.

Open the Developer Tools, click the "···" button in the top-right and find the settings. Within Advanced settings in the bottom-right, check the following:

  • Enable browser chrome and add-on debugging toolboxes
  • Enable remote debugging

Restart Firefox Nightly and open the Browser debugger (Tools -> Browser Tools -> Browser Toolbox). This will open a new window that looks very similar to the common DevTools.

This is your debugger for the parent process (i.e., Browser Toolbox = Parent Toolbox).

The frame selector button, which is left of the three balls "···" will allow you to select between windows. Select browser.xhtml, which is the main browser window. Switching to the Debug pane will let you search files and find the Parent actor you want to debug, as long as they have been already loaded. To ensure the PromptParent actor has been properly initialized, open a new tab on e.g. https://example.com and make it call alert(1) from the normal DevTools console.

[caption id="attachment_223" align="aligncenter" width="1847"]Hitting a breakpoint in Firefox’s parent process using Firefox Developer Tools (left) Hitting a breakpoint in Firefox’s parent process using Firefox Developer Tools left

You should now be able to find PromptParent.jsm (Ctrl+P) and set a debugger breakpoint for all future invocations (see screenshot above). This will allow you to inspect and copy the typical arguments passed to the Prompt JSActor in the parent.

Note: Once you hit a breakpoint, you can enter code into the Developer Console which is then executed within the currently intercepted function.

DevTools Setup - Child Process

Now that we know how to inspect and obtain the parameters which the parent process is expecting for Prompt:Open, let's try and trigger it from a debugged child process: Ensure you are on a typical web page, like https://example.com, so you get the right kind of content child process. Then, through the Tools menu, find the "Browser Content Toolbox". Content here refers to the child process (Content Toolbox = Child Toolbox).

Since every content process might have many windows of the same site associated with it, we need to find the current window. This snippet assumes it is the first tab and gets the Prompt actor for that tab:

actor = tabs[0].content.windowGlobalChild.getActor("Prompt");

Now that we have the actor, we can use the data gathered in the parent process and send the very same data. Or maybe, a variation thereof:

actor.sendQuery("Prompt:Open", {promptType: "alert", title: "👻", modalType: 1, promptPrincipal: null, inPermutUnload: false, _remoteID: "id-lol"});

Invoking JavaScript IPC from Firefox Developer Tools (bottom right) and observing the effects (top right) Invoking JavaScript IPC from Firefox Developer Tools (bottom right) and observing the effects (top right)

In this case, we got away with not sending a reasonable value for promptPrincipal at all. This is certainly not going to be true for all message handlers. For the sake of this blog post, we can just assume that a Principal is the implementation of an Origin (and for background reading, we recommend an explanation of the Principal Objects in our two-series blog post "Understanding Web Security Checks in Firefox": See part 1 and part 2).

In case you wonder why the content process is allowed to send a potentially arbitrary Principal (e.g., the origin): This is currently a known limitation and will be fixed while we are en route to full site-isolation (bug 1505832).

If you want to try to send another, faked origin - maybe from a different website or maybe the most privileged Principal - the one that is bypassing all security checks, the SystemPrincipal, you can use these snippets to replace the promptPrincipal in the IPC message:

const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
otherPrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin("https://evil.test");
systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();

Note that validating the association between process and site is already enforced in debug builds. If you compiled your own Firefox, this will cause the content process to crash.

Revisiting Previous Security Issues

Now that we have the setup in place we can revisit the security vulnerability mentioned above: CVE-2019-11708.

The issue in itself was a typical logic bug: Instead of switching which prompt to open in the parent process, the vulnerable version of this code accepted the URL to an internal prompt page, implemented as an XHTML page. But by invoking this message, the attacker could cause the parent process to open any web-hosted page instead. This allowed them to re-open their content process exploit again in the parent process and escalate to a full compromise.

Let's take a look at  the diff for the security fix to see how we replaced the vulnerable logic and handled the prompt type switching in the parent process (permalink to source).

Handling of untrusted message.data before and after fixing CVE-2019-11708. Handling of untrusted message.data before and after fixing CVE-2019-11708.

You will notice that line 140+ used to accept and use a parameter named uri. This was fixed in a multitude of patches. In addition to only allowing certain dialogs to open in the parent process we also generally disallow opening web-URLs in the parent process.

If you want to try this yourself, download a version of Firefox before 67.0.4 and try sending a Prompt:Open message with an arbitrary URL.

Next Steps

In this blog post, we have given an introduction to Firefox IPC using JavaScript and how to debug the child and the parent process using the Content Toolbox and the Browser Toolbox, respectively. Using this setup, you are now able to simulate a fully compromised child process, audit the message passing in source code and analyze the runtime behavior across multiple processes.

If you are already experienced with Fuzzing and want to analyze how high-level concepts from JavaScript get serialized and deserialized to pass the process boundary, please check our previous blog post on Fuzzing the IPC layer of Firefox.

If you are interested in testing and analyzing the source code at scale, you might also want to look into the CodeQL databases that we publish for all Firefox releases.

If you want to know more about how our developers port legacy MessageManager interfaces to JSActors, you can take another look at our JSActors documentation and at how Mike Conley ported the popup blocker in his Joy of Coding live stream Episode 204.

Finally, we at Mozilla are really interested in the bugs you might find with these techniques - bugs like confused-deputy attacks, where the parent process can be tricked into using its privileges in a way the content process should not be able to (e.g. reading/writing arbitrary files on the filesystem) or UXSS-type attacks, as well as bypasses of exploit mitigations. Note that as of April 2021, we are not enforcing full site-isolation. Bugs that allow one to impersonate another site will not yet be eligible for a bounty. Submit your findings through our bug bounty program and follow us at the @attackndefense Twitter account for more updates.


If you find a mistake in this article, you can submit a pull request on GitHub.

Other posts

  1. Home assistant can not be secured for internet access (Sun 15 December 2024)
  2. Modern solutions against cross-site attacks (Tue 26 November 2024)
  3. Prompt Injections and a demo (Wed 18 September 2024)
  4. The Mozilla Monument in San Francisco (Fri 05 July 2024)
  5. What is mixed content? (Sat 15 June 2024)
  6. How I got a new domain name (Sat 15 June 2024)
  7. How Firefox gives special permissions to some domains (Fri 02 February 2024)
  8. Examine Firefox Inter-Process Communication using JavaScript in 2023 (Mon 17 April 2023)
  9. Origins, Sites and other Terminologies (Sat 14 January 2023)
  10. Finding and Fixing DOM-based XSS with Static Analysis (Mon 02 January 2023)
  11. DOM Clobbering (Mon 12 December 2022)
  12. Neue Methoden für Cross-Origin Isolation: Resource, Opener & Embedding Policies mit COOP, COEP, CORP und CORB (Thu 10 November 2022)
  13. Reference Sheet for Principals in Mozilla Code (Mon 03 August 2020)
  14. Hardening Firefox against Injection Attacks – The Technical Details (Tue 07 July 2020)
  15. Understanding Web Security Checks in Firefox (Part 1) (Wed 10 June 2020)
  16. Help Test Firefox's built-in HTML Sanitizer to protect against UXSS bugs (Fri 06 December 2019)
  17. Remote Code Execution in Firefox beyond memory corruptions (Sun 29 September 2019)
  18. XSS in The Digital #ClimateStrike Widget (Mon 23 September 2019)
  19. Chrome switching the XSSAuditor to filter mode re-enables old attack (Fri 10 May 2019)
  20. Challenge Write-up: Subresource Integrity in Service Workers (Sat 25 March 2017)
  21. Finding the SqueezeBox Radio Default SSH Password (Fri 02 September 2016)
  22. New CSP directive to make Subresource Integrity mandatory (`require-sri-for`) (Thu 02 June 2016)
  23. Firefox OS apps and beyond (Tue 12 April 2016)
  24. Teacher's Pinboard Write-up (Wed 02 December 2015)
  25. A CDN that can not XSS you: Using Subresource Integrity (Sun 19 July 2015)
  26. The Twitter Gazebo (Sat 18 July 2015)
  27. German Firefox 1.0 ad (OCR) (Sun 09 November 2014)
  28. My thoughts on Tor appliances (Tue 14 October 2014)
  29. Subresource Integrity (Sun 05 October 2014)
  30. Revoke App Permissions on Firefox OS (Sun 24 August 2014)
  31. (Self) XSS at Mozilla's internal Phonebook (Fri 23 May 2014)
  32. Tales of Python's Encoding (Mon 17 March 2014)
  33. On the X-Frame-Options Security Header (Thu 12 December 2013)
  34. html2dom (Tue 24 September 2013)
  35. Security Review: HTML sanitizer in Thunderbird (Mon 22 July 2013)
  36. Week 29 2013 (Sun 21 July 2013)
  37. The First Post (Tue 16 July 2013)
π