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.
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.
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
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
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
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)
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.
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.