Firefox is sometimes recommended as a supposedly more secure browser because of its parent company's privacy practices.
This article explains why this notion is not true and enumerates a number of security weaknesses in Firefox's security model
when compared to Chromium. In particular, it covers the less granular process model, weaker sandboxing and lack of modern
exploit mitigations. It is important to decouple privacy from security — this article does not attempt to compare the
privacy practices of each browser but rather their resistance to exploitation.
Section 1 explains the weaker process model and sandboxing architecture. Section 2 examines and compares a number of important exploit mitigations. Section 3 discusses some miscellaneous topics. Finally, section 4 provides links to what other security researchers have said about this topic.
Sandboxing is a technique used to isolate certain programs to prevent a vulnerability in them from compromising
the rest of the system by restricting access to unnecessary resources. All common browsers nowadays include a sandbox
and utilise a multi-process architecture. The browser splits itself up into different processes (e.g. the content
process, GPU process, RDD process, etc.) and sandboxes them individually, strictly adhering to principle of least privilege. It is very important
that a browser uses a sandbox as it processes untrusted input by design, poses enormous attack surface and is one of the
most used applications on the system. Without a sandbox, any exploit in the browser can be used to take over the rest of
the system. Whereas with a sandbox, the attacker would need to chain their exploit with an additional sandbox escape
However, sandboxes are not black and white. Just having a sandbox doesn't do much if it's full of holes. Firefox's sandbox is quite weak for the reasons documented below. Note that this is a non-exhaustive list and the issues below are only a few examples of such weaknesses.
Site isolation is a security feature which was
introduced in Chromium in 2018.
This involved an overhaul in Chromium's multi-process architecture — rather than all websites running within the same process,
this feature now separated each website into its own sandboxed renderer process. This ensures that
a renderer exploit from one website still cannot access the data from another. In addition,
site isolation is necessary for complete protection
against side-channel attacks like Spectre. Operating system mitigations against such attacks only guarantee isolation at the
process boundary; therefore, separating websites into different processes is the only way to fully make use of them. Furthermore,
current browser mitigations, such as
mitigation is through site isolation.
Firefox currently lacks any form of site isolation. Mozilla is working on implementing this with Project Fission and is rolling it out on the Nightly and Beta release channels, but it is still a work in progress and is not yet fit for serious use. Fission in its current state and even once it is initially released will not be anywhere near the level of maturity of Chromium's site isolation and it will take many more years for it to reach that point. Fission still suffers from all the security issues of the baseline content process sandbox, as documented below — it is not a panacea for all sandboxing issues. However, more specific to Fission itself, there are numerous cross-site leaks, allowing a compromised content process to access the data of another and bypass site isolation.
The sandbox is currently only focused on isolating the browser as a whole from the rest of the operating system and even that is still not great, as explained below.
Excluding the issue of site isolation, only the Firefox sandbox on Windows is even comparable to the Chromium sandbox however, it still lacks win32k lockdown. Win32k is a set of dangerous system calls in the NT kernel that expose a lot of attack surface and has historically been the result of numerous vulnerabilities, making it a frequent target for sandbox escapes. Microsoft aimed to lessen this risk by introducing a feature that allows a process to block access to these syscalls, therefore massively reducing attack surface. Chromium implemented this feature in 2016 to strengthen the sandbox, but Firefox has yet to follow suit — Firefox currently only enables this in the socket process (which isn't enabled yet).
Firefox's sandboxing on other platforms, such as Linux, is significantly worse. The restrictions are generally quite permissive and it is even susceptible to various trivial sandbox escape vulnerabilities that span back years, as well as exposing sizable attack surface from within the sandbox.
seccomp-bpf is a sandboxing technology on Linux that allows one to restrict the syscalls accessible by a process which can greatly reduce kernel attack surface and is a core part of most Linux sandboxes. However, Firefox's seccomp filter is substantially less restrictive than the one imposed by Chromium's sandbox and does not restrict anywhere near the same amount of syscalls and their arguments. One example of this is that there is very little filtering of ioctl calls — only TTY-related ioctls are blocked in the content process. This is problematic because ioctl is a particularly powerful syscall that presents a massive kernel attack surface as it comprises of hundreds of different syscalls, somewhat similar to NT's Win32k. Unlike Firefox, Chromium permits only the few ioctls that are necessary in its sandbox which reduces kernel attack surface by a considerable amount. In a similar fashion, Android implemented ioctl filtering in its application sandbox for the same reasons, alongside various other projects with a focus on sandboxing.
On Android, Firefox does not have a multi-process architecture or a sandbox at all beyond the OS app
sandbox, while Chromium uses the
along with a more restrictive seccomp-bpf
In general, Chromium's multi-process architecture is significantly more mature and granular than that of Firefox, allowing it to impose tighter restrictions upon each part of the browser. Examples of processes that are missing from Firefox are listed below. On Firefox, such functionality will be merged into another process, such as the parent or content process, making it considerably harder to enforce strong restrictions.
Later in this article, tables are presented which directly compare some of the mitigations used in Chromium processes to their Firefox equivalents however, they do not list every single process because they would become huge and Firefox would only have a small fraction of processes to be compared with. Instead, only a subset of particularly important processes are included.
Exploit mitigations eliminate entire classes of common vulnerabilities / exploit techniques to prevent or severely hinder exploitation.
Firefox lacks many important mitigations, while Chromium generally excels in this area.
As with the sandboxing, there are many more issues than the ones listed below, but this article does not attempt to be an exhaustive list. You can look through Mozilla's own bug tracker for further examples.
A very common exploit technique is that during exploitation of a buffer overflow
vulnerability, an attacker injects their own malicious code (known as shellcode) into a
part of memory and causes the program to execute it by overwriting critical data, such as
return addresses and function pointers, to hijack the control flow and point to the
aforementioned shellcode, thereby gaining control over the program.
The industry eventually evolved to mitigate this style of attacks by marking writable areas of memory as non-executable and executable areas as non-writable, preventing an attacker from injecting and executing their shellcode. However, an attacker can bypass this by reusing bits of code already present within the program (known as gadgets) outside of the order in which they were originally intended to be used. An attacker can form a chain of such gadgets to achieve near-arbitrary code execution despite the aforementioned protections, utilising techniques such as Return-Oriented Programming (ROP) or Jump-Oriented Programming (JOP).
Attackers often inject their shellcode into writable memory pages and then use these code reuse techniques to transition memory pages to executable (using syscalls such as
VirtualAlloc), consequently allowing it to be executed. Windows 10 implemented a mitigation known as
Arbitrary Code Guard (ACG) which mitigates this by ensuring that all executable memory pages are immutable and can never be made writable.
Another mitigation known as Code Integrity Guard (CIG) is similar to ACG, but it applies to the filesystem instead of memory, ensuring that an attacker cannot execute a malicious program or library on disk by guaranteeing that all binaries loaded into a process must be signed. Together, ACG and CIG enforce a strict W^X policy in both memory and the filesystem.
In 2017, Chromium implemented support for ACG and CIG as
MITIGATION_FORCE_MS_SIGNED_BINS, but Firefox has yet to implement comparable support for
either ACG or CIG.
Firefox only enables ACG and CIG in the socket process (which isn't enabled yet) and CIG in the RDD process.
However, Chromium's application of ACG is still currently limited due to the inherent incompatibility with JIT engines which require memory that is simultaneously writable and executable (JIT is explained in further detail below). ACG is primarily enabled in relatively minor processes, such as the audio, proxy resolver and icon reader processes. If V8's JITless mode is enabled, then Chromium also enables ACG in the renderer process.
* Chromium enables ACG in the renderer process when running in JITless mode.
* Chromium can optionally enable CIG in the network
process if the
NetworkServiceCodeIntegrity feature is enabled.
As briefly mentioned before, code reuse attacks can be used to achieve near-arbitrary code execution by chaining together snippets of code that already
exist in the program. ACG and CIG only mitigate one potential attack vector — creating a ROP/JOP chain to transition mappings to executable. However, an
attacker can still use a pure ROP/JOP chain, relying wholly on the pre-existing gadgets without needing to introduce their own code. This can be mitigated
with Control Flow Integrity (CFI) which severely restricts the
gadgets an attacker is able to make use of, thus disrupting their chain.
CFI usually has 2 parts: forward-edge protection (covering JOP, COP, etc.) and backward-edge protection (covering ROP). CFI implementations can vary significantly. Some CFI implementations only cover either forward-edges or backward-edges. Some are coarse-grained (the attacker has more freeway to execute a larger amount of instructions) rather than fine-grained. Some are probabilistic (they rely on a secret being held and the security properties are not guaranteed) rather than deterministic.
Mozilla has been planning to implement CFI for a while but has yet to make much progress. On Linux, Android and ChromeOS, Chromium enables Clang's fine-grained, forward-edge CFI and on Windows, it enables the coarse-grained, forward-edge Control Flow Guard (CFG). Firefox only enables CFG on Windows which is not as effective as Clang's CFI due to it being coarse-grained rather than fine-grained and does not apply to other platforms.
Untrusted fonts have historically been a common source of vulnerabilities within Windows. As such, Windows includes a mitigation to block untrusted fonts from specific processes
to reduce attack surface. Chromium added support for this in 2016
in the form of
MITIGATION_NONSYSTEM_FONT_DISABLE and enabled it for most child processes however,
Firefox has yet to enable this in any.
|Untrusted Font Blocking|
All common browsers include a JIT compiler to improve performance. There is an inherent security hole in JIT however and that is the requirement of memory that is both writable and executable — a W^X violation which, as discussed above, can aid exploitation. In an attempt to lessen the security risks posed by this feature without sacrificing the performance gains, browsers have adopted JIT hardening techniques to make exploiting the JIT compiler significantly more difficult. In a study on attacking JIT compilers by Chris Rohlf, the hardening techniques implemented in various JIT engines were analysed and compared. This study showed that the JIT engine in Chromium (V8) has substantially better protections than the one used in Firefox (JaegerMonkey). In particular, the mitigations which Chromium used that Firefox did not use include:
Since the publication of the paper, Firefox has not made much progress on adopting these techniques.
Examples of such include constant blinding and NOP insertion.
Firefox attempted to harden the JIT engine by adding a so-called "W^X JIT", but this fails to hinder actual exploits as it is vulnerable to a race window in which an attacker can write their shellcode to the memory mapping when it's writable and wait for the engine to transition it to executable. Additionally, due to the lack of CFI in Firefox, there are also many gadgets available for an attacker to force transition the mapping to executable, such as
VirtualAlloc in the C library. Something similar to Safari's "Bulletproof JIT"
would have been a better approach, utilising two separate mappings — one writable and one executable with the writable mapping being placed at a secret location in memory, concealed
via execute-only memory.
As PaX Team noted in 2015:
> but for this to be safe, the RW mapping should be in a separate process.
note that this is a weakness in the current mprotect based method as well as there's still a nice race window for overwriting the JIT generated code. the only safe way i know of for JIT codegen is to basically fall back to what amounts to AOT codegen, i.e., a separate process (this would make it compatible with MPROTECT in PaX). there's prior art for the V8 engine btw, check out the SDCG work presented at NDSS'15: http://wenke.gtisc.gatech.edu/papers/sdcg.pdf and https://github.com/ChengyuSong/v8-sdcg .
second, since there's no control-flow integrity employed by Firefox (and it can't have one until certain bad code constructs get rewritten) those 'few code paths' you mention are abusable by redirecting control flow there (ExecutableAllocator::makeExecutable is an obvious ROP target if one's lazy to find mprotect itself in libc).
as for the size of the race window, there're two problems with it: first, there're many such windows as there're a lot of users of AutoWritableJitCode (including embeddings) that execute lots of code during those windows (have you actually measured how long those windows are?). second, the window can effectively be extended to arbitrary lengths by first overwriting ExecutableAllocator::nonWritableJitCode to false.
in summary, this is a half-baked security measure that is DOA.
Furthermore, Firefox lacks a hardened memory allocator. Firefox currently uses
mozjemalloc which is a fork of jemalloc. Jemalloc is a performance-oriented
memory allocator — it does not have a focus on security which makes it
very prone to exploitation. Mozjemalloc does add on a few security features to jemalloc which are useful, but they are not enough to
fix the issues present in the overall architecture of the allocator. Chromium instead uses
PartitionAlloc (throughout the entire codebase due to PartitionAlloc-Everywhere) which is
substantially more hardened than mozjemalloc is.
In comparison to mozjemalloc, a few examples of the security features present in PartitionAlloc which do not exist in mozjemalloc are detailed below.
Memory partitioning is an exploit mitigation in which the memory allocator
segregates different types of objects into their own separate, isolated heap so that, for example, a heap overflow would not be able corrupt an object from another heap.
Chromium's PartitionAlloc was explicitly designed with this mitigation in mind (hence the name); strong memory partitioning with no reused memory between partitions is one of PartitionAlloc's core goals.
Firefox's mozjemalloc also eventually implemented some support for partitioning however, memory is reused across partitions, thus severely weakening this feature and allowing it to be bypassed. Therefore, mozjemalloc's implementation of memory partitioning does not guarantee strong isolation to the same extent as PartitionAlloc. In fact, a real-world exploit used by the FBI to unmask users of the Tor Browser was possible due to the lack of memory partitioning.
Traditional heap exploitation techniques often rely on corrupting the memory allocator
metadata. PartitionAlloc stores most metadata out-of-line
in a dedicated region (with the exception of freelist pointers however, they have other protections) rather than having it adjacent to the allocations, thereby significantly
increasing the difficulty of performing such techniques.
Unlike PartitionAlloc, mozjemalloc currently places heap metadata in-line with allocations. As such, the aforementioned techniques are still possible and allocator metadata is easier to corrupt.
Moreover, PartitionAlloc also makes use of guard pages — inaccessible areas of memory that cause an error upon any attempts at accessing it — to surround the metadata, as well as various other allocations to protect them from linear overflows; this is another security feature that mozjemalloc lacks.
In addition, PartitionAlloc is working on many promising upcoming security features, such as their MiraclePtr and *Scan projects to effectively mitigate most use-after-free exploits.
One of the most common classes of memory corruption vulnerabilities is uninitialized memory. Clang has an option to
automatically initialize stack variables with either zero or a specific pattern, thus mitigating this class of vulnerabilities for the stack. Chromium enables this by default on all platforms except
Android, whereas Firefox only enables it in debugging builds for uncovering bugs and is not used in production to mitigate
As for the heap, both PartitionAlloc and mozjemalloc zero-fill allocations so they are equivalent in that regard.
Firefox does have some parts written in Rust, a memory safe language,
but the majority of the browser is still written in memory unsafe languages and the parts that are memory safe do not include important attack
surfaces so this isn't anything substantial and Chromium is working on
switching to memory safe languages too.
Additionally, writing parts in a memory safe language does not necessarily improve security and may even degrade security by allowing for bypasses of exploit mitigations. Some security features are geared towards a particular language and in an environment where different languages are mixed, those features may be bypassed by abusing the other language. For example, when mixing C and Rust code in the same binary with CFI enabled, the integrity of the control flow will be guaranteed in the C code, but the Rust code will remain unchanged because buffer overflows are impossible in Rust anyway. However, this allows an attacker to bypass CFI by exploiting a buffer overflow in the C code and then abusing the lack of protection in the Rust code to hijack the control flow. Mixed binaries can be secure but only if those security features are applied for all languages. Currently, compilers generally don't support this, excluding Windows' Control Flow Guard support in Clang.
Firefox also uses RLBox, but this is only used to sandbox two libraries (Graphite and Ogg) which again, is not anything substantial.
Many security experts also share these views about Firefox.