App Security
Understand the app-level security model on Apple platforms — code signing, entitlements, sandboxing, secure storage, IPC attack surfaces, and how to reason about threats systematically — so you design defenses proportionate to real risk instead of either ignoring security or over-engineering it.
Earlier in this course you learned how to store a secret in the Keychain and how to guard network traffic with ATS and certificate pinning. Those are two specific tools. This lesson zooms out to the whole picture: the layered system that makes an app trustworthy before a single line of your code even runs, and the categories of attack surface every non-trivial app exposes once it starts talking to the outside world. None of this requires you to become a security researcher. It requires you to know what protections the platform already gives you, what protections are entirely your responsibility, and how to think about the difference — a discipline generally called threat modeling, which is where this lesson ends.
Code signing: trust before execution
On Apple platforms, an executable cannot simply run because it exists on disk. Every app, framework, and executable binary must be cryptographically signed with a valid certificate before the operating system will load and execute it. Code signing answers one question: who built this binary, and has it been tampered with since? The signature is computed over the binary's contents, so if a single byte changes after signing — whether through corruption or malicious tampering — the signature no longer matches and the OS refuses to launch it.
The trust is chained, not absolute. Your developer certificate is itself signed by an intermediate certificate, which is signed by Apple's root certificate authority. The operating system trusts Apple's root, trusts the intermediate because Apple vouched for it, and trusts your certificate because the intermediate vouched for it. Break any link in that chain — an expired certificate, a revoked one, a self-signed one that was never countersigned by Apple — and the whole chain of trust collapses, and the app will not run on a properly secured device.
You will rarely write code that manipulates signing directly; it is largely a build and provisioning concern. But you should carry the concept with you as a mental model: signing establishes provenance and integrity, not safety. A properly signed app can still contain vulnerable or malicious logic. Signing proves the binary is what its author shipped — it says nothing about whether that binary behaves well.
Entitlements: capabilities the OS enforces, not just documents
An entitlement is a declared capability that your app has been granted — things like access to a Keychain access group, the ability to receive push notifications, membership in an App Group shared with another app you own, or access to specific hardware like NFC or HealthKit. Entitlements live in a signed property list that is baked into the app at build time, as part of what code signing protects.
The critical thing to understand is that entitlements are not merely documentation you file with Apple for review purposes — they are enforced by the operating system at runtime. If your app was not built with the App Groups entitlement for a particular group identifier, no amount of clever code will let it read that group's shared container; the OS-level access check fails regardless of what your source code attempts. This is a meaningful security property: it means the attack surface of 'what could this app possibly do' is bounded not just by what code ships inside it, but by what capabilities were granted to it and signed into it before it ever ran.
Practically, this means you should request the narrowest set of entitlements your app actually needs. Every entitlement is both a capability and a liability — a wider Keychain access group, for instance, is also a wider set of apps that could potentially read shared secrets if any one of them were compromised. Minimal entitlements are not just tidy engineering; they shrink the blast radius of a mistake.
The sandbox: containment as a first line of defense
Every app on iOS, and every App Store app on macOS, runs inside a sandbox: its own restricted container with its own private slice of the file system. By default, your app cannot read another app's documents, cannot browse the user's file system freely, and cannot see what other apps are installed or doing. This containment exists independent of any specific vulnerability you might imagine — it is a structural precaution, not a patch for a known flaw.
It's worth internalizing why this containment model matters even in the total absence of a known bug. Software has bugs; that is simply a fact of building non-trivial systems. A parsing bug, a buffer overrun in a dependency, a logic error in handling untrusted input — any of these could in principle let an attacker influence what your app does. The sandbox is what limits the damage when that happens. Without sandboxing, a single flaw in one app could cascade into access to every other app's data and the whole file system. With sandboxing, the same flaw is contained to that one app's own restricted corner, dramatically shrinking what a successful attack can actually accomplish.
This is the general security principle of defense in depth: you don't rely on any single defense being perfect, you layer multiple independent defenses so that a failure in one doesn't mean total compromise. The sandbox is the platform's contribution to that layering. Your job is to add your own layers on top — validating input, storing secrets correctly, and being deliberate about what surfaces you expose to the outside world.
Secure storage, revisited
You have already learned the two storage tools and when each applies: UserDefaults is for ordinary, non-sensitive app preferences, stored unencrypted in a property list; the Keychain is for secrets — passwords, tokens, cryptographic keys — stored in an encrypted, access-controlled system service. This lesson does not reteach that mechanism; it asks you to see it as one instance of a broader pattern.
The broader pattern is: classify your data before you decide where it lives. Ask, for each piece of state your app persists, what happens if this leaks, and who could plausibly read it. A cached list of public article titles has essentially no confidentiality requirement. An OAuth refresh token has a very high one. Conflating those two categories — writing a token to UserDefaults because it was convenient, or overusing the Keychain for things that don't need it and only adding friction — is the most common secure-storage mistake in real apps, and it is a design mistake, not a coding mistake. The API calls are simple; the judgment about which API to reach for is the actual skill.
Inter-process communication as an attack surface
Sandboxing keeps apps apart by default, but real apps still need controlled ways to communicate — with each other, with the OS, and with web content. Every one of those channels is a potential attack surface, meaning a point where data crosses a trust boundary and therefore deserves scrutiny before you act on it.
Custom URL schemes let another app, or a web page, hand your app a URL that triggers behavior. App Groups let apps you own share a container, but if that container's contents are ever influenced by anything outside your control, you've reintroduced a boundary you need to defend. The system pasteboard (clipboard) is another surface: anything can be placed there by any app, and reading it back means consuming data of unknown origin. None of these mechanisms are inherently dangerous — they are useful and widely used — but each one is a place where your app receives input it did not generate itself, and that is exactly the definition of untrusted input.
The unifying rule across every IPC surface is the same one that applies to network responses and file input: never assume the data arriving through it is well-formed, expected, or benign, just because it arrived through an Apple API rather than raw sockets. Validate it the same way you would validate a network response.
URL scheme risks: a concrete worked example
A custom URL scheme — say yourapp://reset-password?token=abc123 — is registered in your app's Info.plist and, once registered, can be invoked by literally any other app on the device, or by a link on any web page the user happens to open in Safari. There is no built-in mechanism that restricts who is allowed to construct and fire that URL at your app. That is precisely why you must never trust the parameters of an incoming URL without validating them, no matter how well-formed they look.
Consider a naive password-reset deep link handler that trusts the token parameter blindly and immediately resets credentials or logs the user in based on it. Any other app — or a malicious web page — could construct that exact URL with a guessed or stolen token and drive your app into a privileged state without the legitimate user ever intending it. The fix is not to avoid URL schemes; it's to treat every field of the incoming URL as attacker-controlled input requiring the same validation discipline as a network response: check the token's format and expiry, verify it server-side rather than trusting it at face value locally, and fail closed — refuse to act — on anything malformed or unexpected.
Deep-link validation as a discipline
Deep links are the broader category URL schemes belong to: any mechanism by which an external trigger tells your app to navigate to a specific screen or perform a specific action. Universal Links and custom schemes both fall under this umbrella, and both carry the same obligation — validate and sanitize every parameter before acting on it.
Concretely, this means: check that a product ID parameter actually refers to a product that exists and that the current user is permitted to view, rather than navigating blindly to whatever screen a numeric ID implies. It means bounding what actions a deep link is allowed to trigger at all — a deep link that can navigate to a screen is reasonable; a deep link that can silently delete data or change account settings without any confirmation is a design mistake regardless of how the parameters are validated. Deep-link handling code deserves the same review scrutiny as code that parses a network response, because functionally it is the exact same category of problem: acting on data you did not generate and cannot fully control.
Jailbreak and root assumptions
iOS's sandboxing, code signing, and entitlement enforcement are all part of a security model the OS itself upholds. A jailbroken device is one where that enforcement has been deliberately circumvented — the user, or an attacker with physical access, has removed the guardrails that normally keep apps contained and unmodified. On a jailbroken device, your app cannot assume the OS security model is fully intact: sandbox boundaries may be bypassed, the Keychain may be more exposed, and your binary itself may be more freely inspectable and modifiable at runtime than the signed-and-sandboxed model assumes.
This doesn't mean every app needs sophisticated jailbreak detection — that is a narrow, specific technique with its own trade-offs and this lesson intentionally does not go deep into it. What matters conceptually is humility: your threat model should account for the possibility that some fraction of your users are running on devices where the platform's usual guarantees don't fully hold, and your most sensitive operations — the ones where a compromise would be genuinely serious, such as financial transactions — should not rest entirely on the assumption that the OS's protections are always present and intact.
Reverse engineering awareness
Recall from the Runtime Introspection lesson that Objective-C's messaging model depends on classes, method names, and selectors being real, inspectable runtime data rather than being fully resolved and discarded at compile time. That property, which makes the Objective-C runtime so powerful for legitimate introspection, cuts both ways: it also means a compiled Objective-C binary retains substantial structure that a curious or hostile third party can extract. Class names, method names, and selector strings are largely preserved as readable strings inside the binary, and tools exist that walk the runtime's own class and method tables to reconstruct a near-complete map of your app's object model without ever seeing your source code.
This is a meaningfully different situation from a language or format that strips or obfuscates that structure more aggressively by default. Objective-C's reflective, dynamic nature — the same trait that lets you write powerful, flexible code — means your app is comparatively easy to reverse-engineer relative to more opaque compiled formats. This has one direct, practical consequence for how you write code: never embed long-term secrets — API keys, signing secrets, hardcoded credentials — directly in your binary as if they were safe simply because they are 'compiled.' A determined party can and does extract strings and reconstruct class structure from shipped apps. Secrets belong server-side, or delivered at runtime through a secured channel, never baked into the client as a false sense of protection.
Threat modeling: thinking systematically about risk
Everything in this lesson — signing, entitlements, sandboxing, storage, IPC, deep links, jailbreak assumptions, reverse-engineering exposure — is a piece of vocabulary. Threat modeling is the discipline that ties the vocabulary together into a decision process, and it is the right closing lens for app security as a whole.
A practical threat model asks three questions, in order, for your specific app. First: what assets actually need protecting? Not everything does — public content and cosmetic preferences carry little risk if exposed. User credentials, payment information, private health or personal data, and anything that grants elevated access absolutely do. Second: what attack surfaces exist for reaching those assets — network calls, URL schemes, IPC, local storage, the compiled binary itself? Third, and most important in practice: prioritize your defenses proportionate to realistic risk. A note-taking app with no sensitive data has no business investing in jailbreak detection and certificate pinning; a banking app has every reason to. Security effort spent on unlikely, low-consequence threats is effort not spent on the threats that actually matter — and unnecessary complexity is itself a cost, since it makes the app harder to reason about and more likely to contain its own bugs.
The habit worth carrying forward is not memorizing every mitigation in this lesson — it's asking, for every feature you build, 'what does this expose, to whom, and does that risk matter for this app?' That single habit, applied consistently, catches the overwhelming majority of real-world app security problems long before any specific technique does.
Code Examples
Any app, or any web page, can construct and fire this exact URL. Because the handler logs the user in on the strength of the token alone with no verification, an attacker who merely guesses or observes a token can drive the app into a privileged state.
Common mistake: Trusting url parameters because they arrived through an Apple API, rather than treating them as attacker-controlled input the same way you would treat a raw network payload.
The parameters are format-checked locally, but the actual authority decision — whether this token is genuine and unexpired — is deferred to a server verification call rather than trusted at face value on the device. The UI only proceeds once that verification succeeds, and any malformed input fails closed immediately.
if (token.length == 0 || userID.length == 0 || ![self tokenHasValidFormat:token]) { return NO; }
The access group named here must match one declared in the signed entitlements exactly. Requesting a broader access group than the app actually needs would widen how many other apps could potentially reach these shared secrets if any one of them were compromised — so entitlements should be requested as narrowly as the feature actually requires. This is a conceptual illustration of the plist contents; entitlements themselves are declared in project configuration and signed into the binary, not written as executable Objective-C.
// yourapp://reset-password?token=abc123&userID=42
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
if ([url.host isEqualToString:@"reset-password"]) {
NSDictionary *params = [self queryParamsFromURL:url];
NSString *token = params[@"token"];
NSString *userID = params[@"userID"];
// DANGEROUS: acts immediately on unverified, attacker-reachable input
[self logInUserID:userID usingResetToken:token];
return YES;
}
return NO;
}- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
if (![url.host isEqualToString:@"reset-password"]) {
return NO;
}
NSDictionary *params = [self queryParamsFromURL:url];
NSString *token = params[@"token"];
NSString *userID = params[@"userID"];
if (token.length == 0 || userID.length == 0 || ![self tokenHasValidFormat:token]) {
return NO; // fail closed on malformed input
}
// The token is only a claim; verify it server-side before granting anything.
[self.authService verifyResetToken:token forUserID:userID completion:^(BOOL verified) {
if (verified) {
[self presentPasswordResetScreenForUserID:userID];
} else {
[self presentInvalidLinkError];
}
}];
return YES;
}// Entitlements.plist (conceptual contents, not code executed at runtime)
// <key>keychain-access-groups</key>
// <array>
// <string>$(AppIdentifierPrefix)com.yourcompany.sharedSecrets</string>
// </array>
// <key>com.apple.security.application-groups</key>
// <array>
// <string>group.com.yourcompany.widgetShared</string>
// </array>
// At runtime, the OS enforces these — code cannot access a Keychain
// access group or App Group container that was not declared and signed in.
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccessGroup: @"TEAMID.com.yourcompany.sharedSecrets",
(__bridge id)kSecAttrAccount: @"apiToken",
};Real-world context
Production apps that handle money, health data, or credentials typically combine several of these layers deliberately: narrowly-scoped entitlements, Keychain-only storage for tokens, server-side verification of anything a deep link claims, and a documented threat model reviewed whenever a new feature adds an attack surface. None of these are exotic — they are the accumulated, unglamorous discipline of asking 'what could go wrong here, and does it matter' at every point where untrusted input meets a privileged action.
Trusting a deep link's parameters because it came through UIApplicationDelegate rather than raw network code.
Treat every parameter of an incoming URL or deep link as attacker-controlled input, validate its format, and verify anything security-sensitive server-side.
A custom URL scheme can be invoked by any app or web page — the OS does not restrict who may construct and send it.
Storing an OAuth refresh token in NSUserDefaults because the Keychain API felt like extra ceremony.
Classify data by sensitivity before choosing storage, and route secrets through the Keychain, not UserDefaults.
UserDefaults is unencrypted app-preferences storage; secrets left there are readable by anything that can read the app's container on a compromised or jailbroken device.
Hardcoding a signing secret or API key as an NSString literal in the app because 'it's compiled, so it's hidden.'
Keep long-term secrets server-side or deliver them at runtime through a secured channel; never embed them as literals.
Objective-C binaries retain readable class, method, and string structure, making embedded string literals comparatively easy to extract via reverse engineering.
Adding heavy jailbreak detection and elaborate anti-tampering to a low-risk utility app with no sensitive data.
Scale security investment to the actual assets and realistic risk for that specific app.
Threat modeling means prioritizing defenses proportionate to what's actually at stake — over-engineering security for a low-risk app wastes effort better spent elsewhere and adds needless complexity.
What does code signing on Apple platforms primarily establish? A. That the app contains no bugs B. The provenance and integrity of the binary — who built it and that it hasn't been tampered with since C. That the app has been manually reviewed by Apple's security team D. That the app cannot access the network
B. The provenance and integrity of the binary — who built it and that it hasn't been tampered with since — Signing chains trust from your certificate up through Apple's root CA and detects tampering, but it says nothing about whether the code itself behaves safely.
Why are entitlements enforced by the OS rather than being merely documentation? A. Because Apple reviews entitlements manually before every launch B. Because the OS checks the signed entitlements at runtime and denies capabilities not declared there, regardless of what the code attempts C. Because entitlements are optional and rarely checked D. Because entitlements only affect App Store review, not runtime behavior
B. Because the OS checks the signed entitlements at runtime and denies capabilities not declared there, regardless of what the code attempts — Entitlements are baked into the signed binary and the OS enforces access based on them at runtime — code cannot grant itself an undeclared capability.
Why does app sandboxing matter even when no specific vulnerability is known? A. It improves app launch speed B. It limits the damage any future bug could cause by containing each app to its own restricted data, as a structural precaution rather than a patch C. It is required only for apps that use the Keychain D. It prevents apps from being reverse engineered
B. It limits the damage any future bug could cause by containing each app to its own restricted data, as a structural precaution rather than a patch — Sandboxing is defense in depth: it bounds the blast radius of an unknown future flaw rather than fixing a specific known one.
Why is a custom URL scheme handler a meaningful attack surface? A. Because URL schemes are deprecated on iOS B. Because any app, or a web page, can construct and invoke a registered custom URL scheme, so its parameters must be treated as untrusted input C. Because URL schemes always require the user's biometric approval D. Because URL schemes only work over an encrypted channel
B. Because any app, or a web page, can construct and invoke a registered custom URL scheme, so its parameters must be treated as untrusted input — There is no built-in restriction on who can fire a URL at your registered scheme, so its parameters must be validated exactly like any other untrusted input.
Why are compiled Objective-C binaries comparatively easy to reverse engineer? A. Because Objective-C apps are never code signed B. Because the runtime's dynamic messaging model preserves class names, method names, and selectors as largely readable strings and structure inside the binary C. Because Objective-C source code ships inside every app bundle D. Because entitlements expose the app's full source
B. Because the runtime's dynamic messaging model preserves class names, method names, and selectors as largely readable strings and structure inside the binary — The same reflective structure that enables legitimate runtime introspection also leaves class and method information extractable from a shipped binary.
What is the core practice of threat modeling as described in this lesson? A. Applying the maximum possible security measure to every feature B. Identifying what assets need protecting, what attack surfaces reach them, and prioritizing defenses proportionate to realistic risk C. Disabling all IPC and network features to eliminate risk entirely D. Relying solely on jailbreak detection as the primary defense
B. Identifying what assets need protecting, what attack surfaces reach them, and prioritizing defenses proportionate to realistic risk — Threat modeling is a systematic, proportionate process — not maximal defense everywhere, and not ignoring risk either.