"The container is the trust boundary, except when it isn't," wrote security researcher Wojciech Reguła in a 2021 SecuRing post that has been quietly cited in every subsequent macOS sandbox-escape writeup. He was describing a routing decision Apple made years earlier: that `~/Library/Containers/
The questions below are sequential. Answer the first one wrong and the rest of the chain collapses before you've written a single line of exploit code. Researchers who skip ahead end up with proof-of-concepts that work on their own machine and nowhere else, which is the polite way of saying "doesn't actually work."
Question 1: Does the Target App Already Have a Data Container on Disk?
This is the question every public writeup buries in paragraph nine, and it's the one that decides whether you're doing security research or LARPing as a security researcher. The container at `~/Library/Containers/com.target.bundleid/` is created by `containermanagerd` the first time the sandboxed app launches under a given user account. Before that launch, the directory is empty. The Group Containers folder under `~/Library/Group Containers/` follows a similar lifecycle — created on demand, persistent thereafter.
Here's where it gets interesting, and I mean genuinely interesting, because the timing window matters more than people think. If the user has never launched the target app, the container does not exist. If you create the directory yourself before the app has ever launched, `containermanagerd` will treat your directory as the authoritative container on first launch, because the daemon's check is "is this path populated?" not "did I create this path?" That's the hijack primitive in one sentence. Everything else is plumbing.
If Yes — The Container Already Exists
You are working with a populated trust boundary. The metadata that matters lives in `Data/Library/Preferences/
The realistic primitive here is not container overwrite. It is reading what the legitimate app has cached: Slack tokens in `Containers/com.tinyspeck.slackmacgap/`, Signal database keys in the relevant container, OAuth refresh tokens stored by Electron wrappers that never expected an attacker with read access. These are not exploits in the classic sense. They are credential theft enabled by the fact that the sandbox boundary does not protect against another local process owned by the same user.
If No — The Container Has Never Been Initialized
This is the more powerful branch and the one the published research focuses on, because the absence of a container is the absence of `containermanagerd`'s identity check. The 2020-era CVE-2020-9934 chain — first surfaced by Matt Shockley and elaborated in Reguła's followups — relied on exactly this: pre-populate the container path, plant a malicious `Preferences` plist that triggers behavior the legitimate app will execute on its first launch, and wait. When the user eventually launches the app, the sandbox attaches to a container whose contents the attacker controlled.
Apple's patch for CVE-2020-9934 narrowed the window by hardening the metadata file check, not by changing the routing principle. The routing principle — bundle ID is the key — remains untouched in 2026, because changing it would break every developer-signed app shipped through the App Store in the last fourteen years.
Question 2: Can You Write to ~/Library/Containers Without Full Disk Access?
This is the question that separates a theoretical hijack from a working one, and it's where most blog posts handwave the part that actually matters. `~/Library/Containers` lives inside the user's home directory. It is not SIP-protected. It is not protected by TCC's standard Full Disk Access prompt either — which is the genuinely surprising part of macOS's permission model that nobody outside the security research community seems to register.
TCC, the Transparency Consent and Control framework, gates `~/Documents`, `~/Desktop`, `~/Downloads`, iCloud Drive, the camera, the microphone, screen recording, and accessibility — but it does not gate `~/Library/Containers` for read-write access by other processes owned by the same user. The reasoning, as best as can be reconstructed from WWDC sessions and Apple's developer documentation, is that the container is "the app's own data" and the app's own data was never intended to be a TCC-protected surface. That assumption is the hijack.
If Yes — Your Process Can Touch the Path
Your process is a normal user-owned binary, perhaps a downloaded utility, a malicious Electron wrapper, a script run from the Terminal. You can `mkdir -p ~/Library/Containers/com.target.bundleid/Data/Library/Preferences/` and plant whatever you want, subject to the metadata-file constraint above. You do not need an exploit. You do not need an elevation of privilege. You need the user to have run your code once, with their normal account, no special prompts, no admin password.
This is the part of the macOS threat model that is genuinely under-discussed. The popular framing — "sandboxed apps are contained, non-sandboxed apps are scary" — inverts the actual risk. A sandboxed app cannot reach into another sandboxed app's container. A non-sandboxed user-owned process can. The sandbox protects the inside of the box from the outside; it does not protect the boxes from each other when the boxes are addressed by a name an attacker can also write to.
If No — Your Process Is Itself Sandboxed
If you're starting from within a sandbox — say, you've compromised a sandboxed Mac App Store binary and want to pivot to another app's data — the answer changes. The sandbox profile for almost every App Store app denies arbitrary writes outside its own container. You cannot, from inside `com.attacker.toy`, write to `com.target.bundleid/`. Your pivot path becomes XPC abuse, mach-port confusion, or a separate escape primitive — none of which the container-hijack primitive solves on its own.
The honest version of this branch is: container hijacks are a primitive available to non-sandboxed local code, not a primitive available to already-contained code. The published exploit chains that combined the two — CVE-2021-30659 is the canonical example — always started with code that was either non-sandboxed at launch or had broken out via an unrelated vulnerability first.
Question 3: Is the Hijacked Bundle ID Already Holding TCC Permissions?
This is the question that turns a container hijack from "credential theft via plist reads" into "permission theft via identity reuse." TCC, in the form `tccd` enforces, stores its grants in a SQLite database at `~/Library/Application Support/com.apple.TCC/TCC.db` for user-scoped permissions and at `/Library/Application Support/com.apple.TCC/TCC.db` for system-scoped ones. The grant rows key on bundle identifier and on the code-signing requirement string — but the enforcement check, in practice, has historically had gaps that the research community has documented across at least six CVEs since 2019.
If the target bundle ID already holds, say, Full Disk Access or Screen Recording, the hijack question becomes whether the binary that the hijacked container is currently associated with — your binary, smuggled into the container path under the right identifier — will be treated by `tccd` as authorized to use those grants. The answer in 2026 is "it depends, but less often than it used to."
If Yes — TCC Grants Are Attached to This Bundle ID
Apple's hardening since macOS 11 has progressively tightened the code-signing requirement check that `tccd` performs at grant-time. The grant in the database now stores not just the bundle ID but a `csreq` blob — a serialized Code Signing Requirement — that the requesting process must satisfy. Substituting your own ad-hoc-signed binary into the container path does not, by itself, grant your binary the target's TCC permissions, because your binary will fail the `csreq` check.
What does sometimes work is more subtle: dylib insertion into the legitimate binary's process via `DYLD_INSERT_LIBRARIES` (blocked on hardened-runtime binaries since macOS 10.15, but not on every shipping app), Electron framework abuse where the renderer process inherits TCC grants from the main, and the long tail of apps that ship with `com.apple.security.cs.disable-library-validation` for legitimate reasons. Csaba Fitzl's work documented several of these chains; the public versions are patched, the structural pattern is not.
If No — The Bundle ID Has No Standing Grants
Then the hijack gives you data access (read what's in the container) but not capability access (use the camera, record the screen, read `~/Documents`). For many threat models that is enough. Token theft from a non-privileged Slack container is a perfectly good outcome for an attacker. The capability-theft branch is the more dramatic one and the one that drives the CVE writeups, but it's not the most common payoff in practice.
If You Answered Everything
| Q1: Container exists? | Q2: Can write path? | Q3: TCC grants on bundle ID? | Recommendation |
|---|---|---|---|
| Yes | Yes | Yes | Highest-value chain: credential theft plus possible capability inheritance via dylib/Electron paths. |
| Yes | Yes | No | Credential and token harvesting from container contents; no capability escalation available. |
| Yes | No | Yes | Pivot dead-ends here without a separate sandbox escape. Document the primitive and stop. |
| Yes | No | No | Not a working chain from this position. Find a different starting point. |
| No | Yes | Yes | Classic pre-population hijack — plant container before first launch, wait for user. |
| No | Yes | No | Pre-population gives you data control on first launch but no permission escalation. |
| No | No | Yes | Theoretical only — without write access the hijack does not initiate. |
| No | No | No | Move on. No primitive in this configuration. |
The table is the workbook. The decision tree above is the reasoning. What the published research consistently understates is that rows three through eight are the majority of real-world starting positions — and most of them are not exploitable. The dramatic CVE writeups describe row one and row five. The remaining six rows are why most container-hijack proof-of-concepts demoed at conferences do not reproduce in the wild.
The unsettled question — and this is the one nobody in the published literature has answered cleanly — is whether Apple's incremental hardening of `tccd`'s `csreq` enforcement will eventually close the dylib-insertion and framework-inheritance side channels without breaking the developer ecosystem that depends on them. Library validation exemptions exist for a reason; Electron is a reason; legitimate plugin architectures are a reason. If you have telemetry on how often these side channels are exercised in production by benign software, the research community would like to see it.
FAQ
What exactly is a macOS data container, and how is it different from a sandbox?
The data container is the filesystem location — typically `~/Library/Containers/
Does Full Disk Access protect ~/Library/Containers from other apps?
No. Full Disk Access in TCC gates `~/Documents`, `~/Desktop`, `~/Downloads`, mail data, calendar, contacts, time-machine backups, and a specific allowlist of system paths. `~/Library/Containers` is not on that list. Any user-owned process, including one without any TCC grants, can read and write paths under `~/Library/Containers` for bundle IDs whose containers do not yet exist. This is the surprise that drives most of the research interest.
Has Apple patched the container-hijack primitive entirely?
No. Apple has patched specific chains — CVE-2020-9934, CVE-2021-30659, and others — by hardening the metadata-file check and the `tccd` `csreq` enforcement. The underlying routing principle, that containers are keyed by bundle identifier and that permissions attach to the container rather than to the verified binary, has not changed. Patching it would invalidate developer signing identities and break update paths across the entire App Store catalog.
Is this exploitable remotely, or only by local code?
Local code only, in practice. The container hijack requires write access to `~/Library/Containers`, which requires code execution as the target user. Remote chains that have used this primitive have done so as a post-exploitation step after an initial RCE — typically via a malicious download, a browser exploit, or a phishing-delivered binary. The primitive does not, by itself, cross the network boundary.
Does the App Sandbox protect Mac App Store apps from each other?
Yes, but not in the direction most people assume. App A inside its sandbox cannot reach into App B's container. Both apps' containers, however, can be read or pre-populated by any non-sandboxed user-owned process — meaning a downloaded utility from outside the App Store has more reach into App Store data than another App Store app has. The asymmetry is the often-missed part of the threat model.
How can a developer detect that their app's container has been tampered with?
The pragmatic answer is integrity-checking the container's contents at launch — verifying file hashes, signing critical state files with a key the app derives at first run, and refusing to load preferences that fail validation. Apple does not provide a first-party API for "is this container the one I created?" because the design assumes the container is the app's own data and trusts it. Defensive developers treat the container as semi-trusted input, not as authoritative state.
Are Group Containers vulnerable to the same hijack?
Yes, with caveats. `~/Library/Group Containers/
What macOS version actually fixed the worst of these chains?
macOS 12 (Monterey) closed the metadata-file substitution variant that drove CVE-2020-9934 and tightened `tccd` enforcement in ways that made dylib-side-channel chains harder. macOS 13 and 14 continued the trend without altering the routing principle. As of macOS 15 in 2026, the primitive still exists for the row-five configuration in the table above — non-existent container, writable path, and a target bundle ID with desirable standing grants — though the set of exploitable bundle IDs has narrowed considerably.