---
title: "The Extension Was Sandboxed. The Shared Container Still Crossed the Boundary."
description: "An iOS app and its extensions run in separate containers, but App Groups and Keychain access groups deliberately reconnect them. A defensible review follows each entitlement, producer, shared object, lifecycle transition, and privileged consumer instead of treating code signing as authorization."
date: 2026-09-04
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/ios-app-group-shared-container/
---

## The iOS boundary in 60 seconds

An iOS application can contain several independently executed targets: the visible application, a
Share extension, a notification service extension, a widget, an App Clip, a File Provider extension,
or another system-defined extension point. These targets are signed together and delivered as one
product, but they do not become one runtime process or one private container.

That separation is a strong default. The system launches an extension only for its defined task and
mediates the request from the host application. The containing app and the extension cannot simply
open each other's private application containers.

App Groups deliberately create an exception. Targets that carry the same App Group entitlement can
read and write a shared on-device container, shared preferences, and—in supported configurations—
shared keychain items. The design is useful, but its security meaning is often overstated:

> The entitlement proves that a signed target may reach the shared namespace. It does not prove that
> every value written there is trustworthy for every consumer.

A pentest therefore needs to map four things together: **who can produce shared state, what can be
stored, when another target consumes it, and which effect that consumer can produce**. The bug is
usually not “the sandbox failed.” The bug is that a valid sharing mechanism silently became an
authorization mechanism.

## One product, several security principals

Apple's extension model treats each extension as a separate binary. A host app invokes the extension
through a system-defined extension point; the containing app may not even be running. If the developer
needs indirect data exchange, the product explicitly opts into an App Group.

That creates a graph rather than a hierarchy:

<figure class="diagram">
<svg viewBox="0 0 720 326" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="An iOS host app invokes a sandboxed extension while the containing app and extension separately reach an App Group container and shared keychain namespace, which feed privileged consumers.">
<defs>
<marker id="iag-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker>
</defs>
<rect x="18" y="25" width="150" height="54" rx="8" class="dg-box" />
<text x="32" y="47" class="dg-label">host application</text><text x="32" y="65" class="dg-muted">SYSTEM REQUEST</text>
<line x1="170" y1="52" x2="216" y2="52" class="dg-line" marker-end="url(#iag-a)" />
<rect x="220" y="25" width="156" height="54" rx="8" class="dg-box-accent" />
<text x="234" y="47" class="dg-label">app extension</text><text x="234" y="65" class="dg-accent">SEPARATE SANDBOX</text>
<rect x="18" y="137" width="150" height="54" rx="8" class="dg-box-accent" />
<text x="32" y="159" class="dg-label">containing app</text><text x="32" y="177" class="dg-accent">SEPARATE SANDBOX</text>
<line x1="168" y1="164" x2="251" y2="164" class="dg-line" marker-end="url(#iag-a)" />
<line x1="298" y1="80" x2="298" y2="126" class="dg-line" marker-end="url(#iag-a)" />
<rect x="255" y="130" width="178" height="68" rx="8" class="dg-box" />
<text x="269" y="153" class="dg-label">App Group</text><text x="269" y="171" class="dg-muted">FILES · DEFAULTS · IPC</text><text x="269" y="187" class="dg-accent">SHARED WRITE SURFACE</text>
<line x1="435" y1="153" x2="493" y2="115" class="dg-line" marker-end="url(#iag-a)" />
<line x1="435" y1="176" x2="493" y2="220" class="dg-line" marker-end="url(#iag-a)" />
<rect x="497" y="87" width="202" height="56" rx="8" class="dg-box" />
<text x="511" y="110" class="dg-label">shared keychain item</text><text x="511" y="128" class="dg-muted">ONE ACCESS GROUP</text>
<rect x="497" y="202" width="202" height="56" rx="8" class="dg-box-danger" />
<text x="511" y="225" class="dg-label">consumer operation</text><text x="511" y="243" class="dg-danger">NETWORK · ACCOUNT · FILE</text>
<line x1="598" y1="145" x2="598" y2="198" class="dg-line" marker-end="url(#iag-a)" />
<text x="19" y="302" class="dg-note">Signing grants membership. The consumer still owns semantic validation and authorization.</text>
</svg>
<figcaption>The extension boundary remains intact. The App Group is an intentional bridge whose data still needs a trust model.</figcaption>
</figure>

The host application, containing application, and extension are different roles. “Host” means the
application currently presenting or invoking the extension. “Containing app” means the application
bundle used to deliver it. Confusing those roles leads to bad test cases because the extension receives
host-controlled input while sharing selected state with its containing application.

## What App Groups provide—and what they do not

The `com.apple.security.application-groups` entitlement lists the groups a target may join. Apple
registers iOS App Group identifiers so they are unique. Membership can provide:

- a shared on-volume container;
- a shared `UserDefaults` suite;
- a location for extension-aware background transfer artifacts;
- supported interprocess communication mechanisms;
- use of the App Group identifier as a keychain access group.

Those guarantees are meaningful. A random third-party application cannot invent the entitlement at
runtime and enter another team's registered group. Code signing and provisioning establish which
targets may join.

But group membership is intentionally coarse. If five targets share the same group, the platform does
not know that only the main app should update `account.json`, only the notification extension should
write `pending-message.plist`, or the widget should receive a read-only projection. At the file-system
level, each entitled member may have read/write access to the shared container.

This is where the application must add a semantic policy:

| Platform fact                                      | Application question that remains                                               |
| -------------------------------------------------- | ------------------------------------------------------------------------------- |
| The target is signed and provisioned for the group | Is this target an accepted producer for this object?                            |
| The shared file is inside the group container      | Is its format, owner, version, size, and state transition valid?                |
| The keychain item is in a reachable access group   | Should this target receive the same credential or key material?                 |
| Data Protection encrypts the file at rest          | Is it available in the correct device-lock state and to the correct workflow?   |
| A background transfer wrote into the group         | Was the response authenticated and bound to the initiating account and request? |

Encryption, sandboxing, and code signing remain effective even when the application makes the wrong
decision about shared data. A report should identify the failed application invariant instead of
claiming that the platform control was bypassed.

## Begin with the signed target inventory

The most useful first artifact is not a list of URL schemes. It is a target-to-entitlement matrix.
For an authorized review of a customer-provided `.ipa`, extract the archive in an isolated workspace
and inventory every executable before launching the app.

Read-only examples:

```bash
unzip -q CustomerApp.ipa -d extracted
find extracted/Payload -type d -name '*.appex' -print
find extracted/Payload -type f -perm -111 -print
codesign -d --entitlements :- extracted/Payload/CustomerApp.app
codesign -dvvv extracted/Payload/CustomerApp.app
```

Repeat entitlement inspection for each `.appex` and embedded executable. Preserve the original IPA
hash, the extracted target path, and the signing output. Do not infer an extension's entitlements from
the main application.

Record at least:

| Target               | Extension point      | App Groups        | Keychain groups   | Protected resources   | Inputs               | Outputs                  |
| -------------------- | -------------------- | ----------------- | ----------------- | --------------------- | -------------------- | ------------------------ |
| Main app             | application          | exact identifiers | exact identifiers | declared capabilities | user, links, network | files, secrets, requests |
| Share extension      | share                | exact identifiers | exact identifiers | host-provided items   | extension context    | shared drafts            |
| Notification service | notification service | exact identifiers | exact identifiers | network if used       | APNs payload         | rendered content, cache  |
| Widget               | widget               | exact identifiers | exact identifiers | timeline data         | shared snapshot      | UI projection            |
| File Provider        | file provider        | exact identifiers | exact identifiers | document domain       | remote/local items   | shared file state        |

The goal is to reveal asymmetry. A small extension may parse hostile content but share a writable
container with a main app that holds authenticated network sessions, private user data, or stronger
business capabilities.

<figure class="evidence">
  <picture>
    <source
      type="image/webp"
      srcset="/images/posts/responsive/ios-app-group-evidence-target-inventory-768.webp 768w, /images/posts/responsive/ios-app-group-evidence-target-inventory-1200.webp 1200w, /images/posts/ios-app-group-evidence-target-inventory.webp 1600w"
      sizes="(max-width: 820px) 94vw, 720px"
    />
    <img
      src="/images/posts/ios-app-group-evidence-target-inventory.webp"
      alt="Evidence plate showing a synthetic IPA subset with a main app, Share extension, and Notification Service as separate signed targets under one App Group."
      loading="lazy"
      decoding="async"
      width="1600"
      height="1000"
    />
  </picture>
  <figcaption>
    <span>Evidence / 01</span>
    Three executables join one App Group namespace. Inventory each target’s entitlements and effects separately.
  </figcaption>
</figure>

## Treat every shared object as a protocol

A shared plist or SQLite row may look like storage, but between independently scheduled processes it
is a protocol. It has producers, consumers, a schema, ordering rules, and failure behavior.

A trustworthy shared object normally needs:

- a strict schema and explicit version;
- bounds on strings, collections, files, and decoded payloads;
- an account or tenant binding that is not taken only from the object itself;
- a state-machine transition the consumer can verify;
- safe path construction rooted in the expected container;
- atomic replacement or transaction semantics;
- rejection of symlinks or file types the workflow does not require;
- a freshness or replay rule where old intent must not be processed twice;
- provenance strong enough for the consequence of the operation.

Consider an extension that writes this conceptual job:

```json
{
  "operation": "upload",
  "account": "A-104",
  "relativePath": "Exports/report.pdf",
  "destination": "case-781"
}
```

The main app must not interpret “the file is in our App Group” as authorization to perform the upload.
It should derive the active account from trusted session state, resolve the path beneath an expected
root, reject unsupported object types, bind the destination to policy, and make replay visible. The
shared object may carry intent; it should not manufacture authority.

## Keychain sharing is a capability decision

On iOS, an app can reach its own keychain items and items in access groups granted through its signed
entitlements. A keychain item belongs to one access group. If a target does not possess the required
entitlement, the platform rejects access.

This is a strong boundary, but the group design decides its breadth. Adding the same access group to a
widget, notification extension, App Clip, and main application may give every target the technical
ability to query items in that group. The review should ask whether they need the same secret—not only
whether Xcode made the capability available.

Separate these questions:

1. **Reachability:** which signed targets can address the access group?
2. **Item placement:** which specific items were written into that group rather than a private group?
3. **Accessibility:** under which device and authentication conditions can the item be returned?
4. **Purpose:** does the target need the raw credential, or only a narrow derived result?
5. **Revocation:** what happens to shared credentials after logout, account switch, app removal, or
   extension disablement?

`kSecAttrAccessible` and access-control flags govern when an item is available relative to device state
and user authentication. They do not reduce an unnecessarily broad set of entitled targets. Conversely,
a narrow access group does not repair an item that remains available in an inappropriate lock state.
Both dimensions belong in the evidence.

<figure class="evidence">
  <picture>
    <source
      type="image/webp"
      srcset="/images/posts/responsive/ios-app-group-evidence-capability-matrix-768.webp 768w, /images/posts/responsive/ios-app-group-evidence-capability-matrix-1200.webp 1200w, /images/posts/ios-app-group-evidence-capability-matrix.webp 1600w"
      sizes="(max-width: 820px) 94vw, 720px"
    />
    <img
      src="/images/posts/ios-app-group-evidence-capability-matrix.webp"
      alt="Evidence plate comparing App Group membership and keychain reachability across a main app, Share extension, and Notification Service."
      loading="lazy"
      decoding="async"
      width="1600"
      height="1000"
    />
  </picture>
  <figcaption>
    <span>Evidence / 02</span>
    App Group membership is shared; keychain and session strength are not. Map producers and privileged consumers separately.
  </figcaption>
</figure>

## Lifecycle is part of the attack surface

Extensions are short-lived and can be terminated after they complete a request. The containing app and
extension can also access the shared container at different times. That creates states that a purely
interactive test misses:

- the extension writes while the main app is suspended;
- the main app upgrades a schema while an older extension artifact remains;
- an account changes between production and consumption of a queued job;
- a background URL session completes after logout;
- two processes update the same shared preferences or database record;
- one app in the group is removed while another remains installed;
- protected data becomes unavailable or available across device lock transitions.

Test these as state transitions, not as random race attempts. Use synthetic accounts and non-sensitive
fixtures on a customer-approved test device. Capture the precondition, producer, object hash, timestamps,
lock state, consuming process, and resulting effect.

<figure class="diagram">
<svg viewBox="0 0 720 314" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A shared object moves from extension input through schema, account, path, freshness and state gates before a consumer effect; failed validation is rejected and logged.">
<defs>
<marker id="iag-b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker>
</defs>
<rect x="18" y="35" width="132" height="54" rx="8" class="dg-box-accent" />
<text x="32" y="57" class="dg-label">extension input</text><text x="32" y="75" class="dg-accent">UNTRUSTED INTENT</text>
<line x1="152" y1="62" x2="188" y2="62" class="dg-line" marker-end="url(#iag-b)" />
<rect x="192" y="27" width="150" height="70" rx="8" class="dg-box" />
<text x="206" y="49" class="dg-label">shared object</text><text x="206" y="67" class="dg-muted">VERSION · HASH</text><text x="206" y="84" class="dg-muted">ATOMIC STATE</text>
<line x1="344" y1="62" x2="380" y2="62" class="dg-line" marker-end="url(#iag-b)" />
<rect x="384" y="18" width="152" height="88" rx="8" class="dg-box" />
<text x="398" y="40" class="dg-label">consumer gates</text><text x="398" y="58" class="dg-muted">SCHEMA · ACCOUNT</text><text x="398" y="76" class="dg-muted">PATH · FRESHNESS</text><text x="398" y="94" class="dg-muted">STATE · POLICY</text>
<line x1="538" y1="62" x2="574" y2="62" class="dg-line" marker-end="url(#iag-b)" />
<rect x="578" y="35" width="124" height="54" rx="8" class="dg-box-accent" />
<text x="592" y="57" class="dg-label">bounded effect</text><text x="592" y="75" class="dg-accent">AUTHORIZED</text>
<line x1="460" y1="108" x2="460" y2="161" class="dg-line" marker-end="url(#iag-b)" />
<rect x="384" y="165" width="152" height="58" rx="8" class="dg-box-danger" />
<text x="398" y="188" class="dg-label">reject + record</text><text x="398" y="206" class="dg-danger">NO SIDE EFFECT</text>
<line x1="460" y1="225" x2="460" y2="262" class="dg-line" marker-end="url(#iag-b)" />
<rect x="345" y="266" width="230" height="34" rx="8" class="dg-box" />
<text x="359" y="288" class="dg-note">producer · object hash · reason · effect</text>
</svg>
<figcaption>The consumer—not the shared container—must convert shared intent into a bounded, attributable effect.</figcaption>
</figure>

<figure class="evidence">
  <picture>
    <source
      type="image/webp"
      srcset="/images/posts/responsive/ios-app-group-evidence-consumer-decision-768.webp 768w, /images/posts/responsive/ios-app-group-evidence-consumer-decision-1200.webp 1200w, /images/posts/ios-app-group-evidence-consumer-decision.webp 1600w"
      sizes="(max-width: 820px) 94vw, 720px"
    />
    <img
      src="/images/posts/ios-app-group-evidence-consumer-decision.webp"
      alt="Evidence plate showing ALLOW when a shared upload job matches the active session account and DENY when the account field is spoofed."
      loading="lazy"
      decoding="async"
      width="1600"
      height="1000"
    />
  </picture>
  <figcaption>
    <span>Evidence / 03</span>
    Same App Group queue, different consumer decisions: session-bound account allows a bounded effect; spoofed account is denied with no upload.
  </figcaption>
</figure>

## A defensible test sequence

### 1. Establish scope and test planes

Use only a customer-provided build, approved bundle identifiers, test accounts, and a designated test
device. Decide which observations are possible through static review, Simulator, a normal physical
device, and instrumented or research devices. Do not present Simulator behavior as proof of a hardware-
backed keychain or physical-device Data Protection outcome.

### 2. Inventory targets and effective entitlements

Build the target matrix from signed artifacts. Compare provisioning intent with the entitlements on
each executable. Highlight every shared App Group and keychain access group, then list all members.

### 3. Classify shared objects

For each shared file, database, preference suite, background session, and keychain item, record:

- producer targets and consumer targets;
- whether the data is secret, authoritative, or merely a cache;
- expected schema and size;
- account and device-state binding;
- the effect of corruption, replacement, replay, deletion, or delay.

### 4. Exercise one controlled mutation at a time

Begin with harmless synthetic values: malformed versions, oversized but bounded fields, a stale account
identifier, an unexpected relative path, or replayed job identifier. Never test destructive operations
against real user data. Observe whether the consumer rejects before producing a network, account, file,
or cryptographic effect.

### 5. Repeat across lifecycle boundaries

Run the same bounded case after suspension, relaunch, device lock/unlock, account switch, and background
completion where relevant. The conclusion should name the exact state transition that changed behavior.

### 6. Prove the effect and the fix

A writable shared file alone is expected platform behavior, not a vulnerability. Evidence must connect
the controlled mutation to an unauthorized or unsafe consumer effect. After remediation, repeat the
same case and show rejection, attribution, and absence of the prior side effect.

## Common false positives

### “The extension can read the App Group container”

That is the declared purpose of group membership. It becomes a finding only when membership is broader
than required, sensitive objects are placed in the wrong namespace, or a consumer trusts another
member's data beyond its intended authority.

### “The shared file is not encrypted by the app”

iOS Data Protection already provides platform encryption classes for files and databases. The useful
question is which class applies, when data is available, whether backups or synchronization matter, and
whether an authorized target can misuse plaintext after the system returns it.

### “A keychain query succeeds from two targets”

Success may be the designed effect of a shared access group. Establish the item, group, target set,
accessibility, user-presence requirement, and business need before assigning impact.

### “The extension bundle is inside the app, so it is trusted input”

Its code is part of the signed product, but the extension may parse data supplied by another host app,
a notification payload, a document provider, or network content. Signed code can still become a
producer of attacker-influenced state.

### “Jailbreak-only access proves production exploitability”

A research device can reveal implementation details and help create hypotheses. It does not prove that
an unprivileged production application can cross the same boundary. Report the observation plane and
the missing prerequisite explicitly.

## Evidence matrix

| Claim                                   | Minimum evidence                                                  | Stronger evidence                                                   | Do not claim from this alone                    |
| --------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------- |
| A target belongs to an App Group        | Effective signed entitlement from that target                     | Provisioning comparison plus successful controlled container access | That every shared object is sensitive or unsafe |
| An extension controls a shared field    | Controlled synthetic write and object diff                        | Producer trace with object hash and timestamps                      | That a privileged effect occurred               |
| A consumer trusts unsafe shared state   | Repeatable mutation changes a consumer decision                   | Trace joining producer, object, consumer, and effect                | Sandbox escape or arbitrary code execution      |
| Keychain scope is broader than required | Access-group membership and reachable test item                   | Target-by-item necessity analysis and lock-state results            | Extraction by unrelated third-party apps        |
| Data Protection behavior is weak        | Physical-device result with documented lock state and file class  | Repeated cold/locked/unlocked measurements                          | Hardware-backed behavior from Simulator alone   |
| Background completion crosses logout    | Synthetic transfer initiated before logout and consumed afterward | Account-bound request/response trace and remediation replay         | Exposure of real customer data without evidence |
| The fix works                           | Original case rejected before side effect                         | Negative tests, telemetry, and regression coverage                  | That all shared-container risks are eliminated  |

## What remediation should look like

The strongest fix usually reduces both membership and meaning:

1. Give each target only the App Groups and keychain groups it needs.
2. Keep credentials private unless another target truly needs the raw item.
3. Split shared namespaces by purpose instead of placing every target in one universal group.
4. Treat group files as untrusted protocol messages: strict schema, bounds, version, account binding,
   path confinement, freshness, and atomic state transitions.
5. Let a higher-authority consumer derive authorization from its own trusted state.
6. Store the smallest possible projection for widgets and extensions.
7. Clear or rotate shared state on logout, account switch, and membership changes.
8. Log enough identifiers to join producer, object, decision, and effect without recording secrets.
9. Test lock-state and background-session behavior on a physical device.
10. Preserve a regression fixture for every confirmed boundary failure.

Sometimes the correct design is not another validation check. It is removing the shared secret,
replacing a writable job file with a narrow brokered operation, or giving an extension a read-only
projection that cannot authorize anything.

## My conclusion

iOS sandboxing answers an important question: what resources may this executable reach? App Groups and
keychain access groups answer another: which signed targets may intentionally share selected resources?
Neither mechanism knows the application's business invariant.

The defensible security question is therefore:

> Can a lower-context producer place data into a shared namespace that a different target converts into
> a higher-consequence effect without independently validating provenance, state, and authorization?

That question scales beyond one extension type. It applies to widgets, notifications, App Clips, File
Providers, background transfers, and future Apple platform targets. It also gives this research series
a stable method: map the executable, map its entitlements, follow the shared object, and prove the final
effect.

## Sources

- [Apple Platform Security: App security overview](https://support.apple.com/guide/security/app-security-overview-sec35dd877d0/web)
- [Apple Platform Security: App protection and App Groups](https://support.apple.com/en-gb/guide/security/sec1a976c067/web)
- [Apple Developer: App Groups entitlement](https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.security.application-groups)
- [Apple Developer: Configuring App Groups](https://developer.apple.com/documentation/xcode/configuring-app-groups)
- [Apple Developer: Sharing keychain items among apps](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps)
- [Apple Developer: Restricting keychain item accessibility](https://developer.apple.com/documentation/security/restricting-keychain-item-accessibility)
- [Apple Developer: App Extension architecture](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionOverview.html)
- [Apple Developer: Sharing data with a containing app](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html)
- [Apple Developer: `sharedContainerIdentifier`](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/sharedcontaineridentifier)