DirtyDict: Escaping the macOS Sandbox and wrecking havoc
Introduction
Imagine a vulnerability on macOS that would allow a local attacker to read and write to any file on disk, even from within the App Sandbox. That would be quite the very powerful vulnerability. What I found was close to this, although there are a few caveats:
- it only worked on text files,
- its writing ability was limited to appending to the end of the files,
- it could only access files as the logged in user (so no
root-owned files), and - (most unfortunately for any attacker) read files would be returned with their line order scrambled, and append-to files would have their line order scrambled after the appending.
There was also some caching involved wherein the returned lines may not have matched what was in the target file at that exact time. All of this might seem to severely limit what can be dome with this, but it turns out there are some caveats to the above caveats. But first, I need to explain some things.
NSSpellServer
This vulnerability (really a series of vulnerabilities) that I'm calling DirtyDict really starts with the NSSpellServer API. This API allows developers to implement their own spell-checkers that can be used via the corollary NSSpellChecker API. I doubt these API's are used all that much. They have their origins in NeXTSTEP, likely having existed since the beginning of Mac OS X. The NSSpellServer API itself also had a path traversal vulnerability that was the culprit behind several CVE's. But before I talk about those, what does any of this have to do with the App Sandbox?
The (App) Sandbox
What is the (App) Sandbox?
The App Sandbox is actually a subset descriptor of the general concept of sandboxing on macOS. macOS (as well as other Apple OS's) contains a Sandbox kernel module. It uses the TrustedBSD MAC Framework I wrote about in one of my previous articles to restrict specific operations. It allows for the creation of profiles that are attached to processes. These profiles are actually highly configurable… just not by you. Many of Apple's own binaries have their own custom sandbox profiles, suited to their needs. But really, the only one third-party developers can use is the App Sandbox.
SBPL
One undocumented representation of sandbox profiles is a Scheme-like language many researchers have taken to calling SBPL (although there doesn't seem to be agreement on if this stands for Sandbox Policy Language
or Sandbox Profile language
). Regardless, the name is likely taken from the undocumented com.apple.security.temporary-exception.sbpl entitlement, which allows for additional policy rules to be attached to a process. And, despite this entitlement being undocumented, I have seen it be used by major third-party applications like Microsoft Office and still be distributed in the Mac App Store. It's unclear to me if Microsoft's size as a company is what has given them enough leverage to do this.
How was this a sandbox escape?
The SBPL code defining many sandbox profiles can be found in /System/Library/Sandbox/Profiles. The files within are simple text files with the .sb extension, and the application.sb file contains the definition of the profile used by all apps that use the App Sandbox. The NSSpellServer API includes a method to register a spell checker with a language string and a vendor string. While the documentation doesn't explain this, using this method ultimately results in the registration of a Mach service with a name in the format of {language} ({vendor})_OpenStep. Finally, the code inside application.sb ultimately results in the below rule that allows for a sandboxed app to look up any spell server with this name format:
(allow mach-lookup (global-name-regex "_OpenStep$"))
How did DirtyDict work?
Spell servers registered with the NSSpellServer API are, behind the scenes, implemented using Distributed Objects, a long-since-deprecated set of API's that allowed for Objective-C objects to be used across processes without changing syntax. This API, based around NSConnection) was purposely replaced with XPC (more specifically, NSXPCConnection). As Quinn from Apple Developer Technical Support one put it in a support thread: The goal of DO was to hide the IPC as much as possible. That’s not a goal of XPC
.
As spell servers are just distributed objects, that makes it possible to use the NSConnection API's to call methods on the remote servers, even undocumented ones. Two undocumented methods are where the path traversal vulnerability was found (or rather, both their code paths ultimately led to the same place):
_appendWord:toDictionary:_wordsInDictionary:
The dictionary
parameter in both methods are ultimately used to form a path to a what is meant to be a dictionary of words, one per line. As for the line scrambling
I mentioned earlier, this is because spell servers return the words/lines in a specifically-sorted order. Spell servers also re-sort the text files after appending a new word/line (although, in some cases, this could be avoided). The root vulnerability was that nowhere in these code paths did the code check the dictionary name for character sequences like .., allowing for path traversal and the reading of / appending to of arbitrary text files.
Exploiting DirtyDict
Reading from files
The ability to read from files is arguably the more useful primitive of DirtyDict (compared to the ability to write to files). There are many configuration files, often in known locations, where line order doesn't matter that much. For example Chromium-based browsers store many of their config files (including user profiles) in minified JSON files that are just one line. Even un-minified JSON files with known schemas can often be more-or-less deciphered even with their lines scrambled.
More worrying for developers, .env files containing critical secrets could be compromised through this (as they often include one value per line). While an attacker would have to know the direct path to those files, they could read their editor's config files or even the command line history (~/.zsh_history) to find where project directories live and go from there. They could even potentially utilize local AI to determine directories of interest from the command line history.
All of this would likely fly under the radar of most EDR tools. It's abusing a lower-level (and technically deprecated) IPC interface to proxy file access through what are likely system processes (spell servers built into macOS itself). Conversely, if a piece of malware were to directly reach out to read from files, that would likely be easily caught by any competent security product. I'm unaware of how closely EDR tools monitor the file accesses of system processes, but I wouldn't be surprised if they were given more leeway.
Writing to files
Initial Possibilities
While writing to files might seem like the less useful primitive due to it being append-only and because of the line scrambling, the latter can be avoided in some cases and even potentially used to an advantage. For the former case, AppleSpell (the main spell server on macOS) actually uses a separate process to perform the post-append sorting. An unsandboxed app could simply wait for such a process to appear and immediately send it a SIGKILL signal. And while an unsandboxed app could likely also reach out to files directly anyway, the benefit of proxying these file writes through spell servers is clear (as discussed above).
For the latter case (using the sorting to an advantage), its theoretically possible an attacker, if they knew the contents of a target file, could use DirtyDict to append several carefully-crafted lines to a file so that the sorted result is advantageous. For example, if it is a code file that an end user would never look at directly, they could simply append new code lines (prefixed with comments using specific characters to ensure the lines are sorted in the right order) as well as lines that open and close a multi-line comment in such a way that they wrap the original code. For context, I didn't try this myself, but I still do see the possibility.
Electron, the App Sandbox, and You
Electron is an app development framework built on top of Chromium. I previously wrote about Electron and how apps written with it are often vulnerable to code-injection unless specific security features are used. The unfortunate thing, as I mentioned in that article, is that it's a widely-held ecosystem policy to ignore local attacks (going all the way up to Chromium). I explain in that article how, while that might make sense on other platforms, it makes less sense on macOS due to its additional security features.
Some of you may have seen my social media post about how Discord (an Electron app) is vulnerable to having a keylogger injected into it. This is actually what I had reported to Discord and received the reply I quote in my previous article. My discovery of this was prompted by previous research by Wojciech Reguła as well as this tweet by Theo. When I found DirtyDict, I realized its potential for Electron code-injection attacks like this one. While I didn't take the time to create a fully-sandbox-capable version, I was able to inject a keylogger into Discord from an unsandboxed app using DirtyDict. And while DirtyDict has been patched on macOS, likely many Electron apps (like Discord) still remain vulnerable to code injection.
A Complete Sandbox Escape
While reading from and writing to files are good primitives, we can actually do more with DirtyDict. Using a tactic I learned from this post from Microsoft Threat Intelligence, I figured out I could write commands to a shell config file, then open Terminal, and those commands would be executed. As explained in the previous link, shell config files contain what are essentially commands to run before a shell session (which Terminal uses). Some of you may recognize this as T1546.004 from the MITRE ATT&CK® framework.
Another tatic I used was T1564.003 (Hidden Window). Inside their Info.plist's, apps can define themselves as UI elements,
in which case they will not have Dock icons. This behavior can actually be given to any app at launch using undocumented options and, in conjunction with other options, can allow for completely silent launching of apps. I figured out this could be used to launch Terminal silently, allowing for a completely invisible (to the end user) sandbox escape.
History of DirtyDict
Mickey Jin
While I am the first to publicly disclose DirtyDict, I was not the first to find it. That honor goes to Mickey Jin. Mickey is an independent security researcher who is a bit of a legend in the Apple security space. He has over 300 CVE's from Apple from a little over five years of research (as of writing). It was a bug he found in 2023 that, to my knowlege, was the first CVE that can be attributed to DirtyDict.
CVE-2023-32444 - Mickey Jin
CVE-2023-32444 was a bug found by Mickey Jin in AppleSpell. Mickey privately shared me his notes and gave me permission to speak on this publicly. He found an XPC service (one that used the dictionary-based API) that ultimately ended up at the same vulnerable code path in the NSSpellServer API I mentioned earlier. This API was likely written to be a modern replacement to the older NSConnection interface (although it appears all it ultimately does is wrap that older interface). Apple added anti-path-traversal logic to the XPC interface in AppleSpell, but the NSConnection interface was left unpatched.
CVE-2024-27887 - Mickey Jin
CVE-2024-27887 was a later bug that Mickey found which he described to me as a bypass to the patches for CVE-2023-32444. While he declined to shared further details with me, I did notice (through historical forum posts) that, around the time of the patch, AppleSpell started to use a Group Container. I have my theories on how this might be related to the CVE, but I'll leave disclosure of this bug up to Mickey.
CVE-2025-43266 - Noah Gregory
CVE-2025-43266 was the first DirtyDict bug I found. I was searching through Mach services and came across the NSConnection ones (there are multiple) from AppleSpell. I traced them back to the NSSpellServer API and found the path traversal vulnerability. I also discovered the aforementioned fact that AppleSpell uses a separate binary for post-append file line sorting. To patch this vulnerability, Apple added anti-path-traversal logic to the NSConnection interface in AppleSpell.
CVE-2025-43190 - Noah Gregory
CVE-2025-43190 was the second DirtyDict bug I found. It turns out there's a second spell server binary on macOS called OpenSpell. It's normally not active, but it can easily be opened with the following command:
open /System/Library/Services/OpenSpell.service
OpenSpell behaved a bit differently than AppleSpell. It did not use a Group Container, and it didn't use a secondary binary for post-append file line sorting (instead opting to do so in-process). Because it is its own binary, separate from AppleSpell, it too needed to be patched with anti-path-traversal logic.
CVE-2025-43469 - Mickey Kin
CVE-2025-43469 is a mystery to me. Apple listed it under the NSSpellChecker component (and also, as I'm finding out while writing this, under the AppleMobileFileIntegrity component for some reason), so I'm including it here for completeness. However, I have no further information than that. It might not even be related to DirtyDict. I'll let Mickey decide if he ever wants to disclose the details of this bug.
CVE-2025-43518 - Noah Gregory
CVE-2025-43518 was the third DirtyDict bug I found, and hopefully the last one ever. I told Apple they should probably patch the underlying NSSpellServer API instead of just patching their first-party spell servers, as third-party spell servers remained vulnerable as they (likely) wouldn't have the anti-path-traversal logic Apple added to their own servers. Apple thankfully agreed with me and added anti-path-traversal logic directly into the underlying API. This should hopefully be the end of the DirtyDict saga.
Conclusions
This article has been a long time coming. I was waiting for at least the underlying NSSpellServer API to be patched before writing about this, as I didn't want to expose end users to unneeded risk. Really, my experience researching DirtyDict (and all the tangential topics I found myself looking into) has taught me how macOS sits high above other platforms when it comes to protections against local attacks. And while Apple has frustratingly slashed bounty rewards for reporting such attacks, the security features they have in macOS are almost unparalleled compared to other desktop platforms.
The intentional dismissal of local attacks that I pushed back against in my previous article likely stems from the situation on other platforms like Windows and Linux, where local access = game over
is still often an unfortunate reality. And while proxying file access through a system process can be beneficial in evading EDR tools, breaking the sandbox barrier is another capability that doesn't have much of a common parallel that I'm aware of on other platforms.
It's frustrating to me, because I believe filesystem security can be made fundamentally better than it is today, but most of the parties involved don't care enough or refuse to put work into it. Apple could allow end developers to use SBPL to describe more complex sandbox profiles for their apps. Even without that, developers could do more to protect their sensitive files. Beyond this, I believe users can and should be more educated about filesystem security. I hope for a day where local access = game over
is no longer something we shrug our shoulders at, but something we actively work towards making more and more untrue.
To keep up to date with what I post here, remember to watch this space.