iOS App Security Audit: 10 Issues a Standard Code Review Can Miss
A security-focused review goes beyond Swift source to inspect Keychain accessibility, logs, local data, ATS, runtime integrity, app-switcher snapshots, trust policy, entitlements, file protection, and privacy manifests.
A normal pull-request review is good at finding logic errors, unclear ownership, and maintainability problems in the code being changed. It can still miss security decisions that live outside that diff: Keychain accessibility, entitlements, Info.plist, local-file protection, dependency manifests, app lifecycle behavior, and the effective trust policy of every network request.
That is the practical distinction between code review and a security audit. An audit treats the shipped app as a system and checks whether its configuration and runtime behavior match its threat model.
Why ordinary code review has blind spots
Reviewers usually see a bounded set of Swift changes. The released product also includes build settings, provisioning entitlements, privacy declarations, third-party binaries, cached files, logs, database sidecars, and UI snapshots created by the operating system.
Security review therefore combines source inspection with configuration review, dependency inventory, archive inspection, and focused runtime testing. The following ten areas are common places where the two review methods produce different findings.
1. Keychain items with the wrong accessibility policy
Using Keychain is only the first decision. The item’s kSecAttrAccessible value determines when it can be read and whether it can migrate to another device.
Apple recommends choosing the most restrictive policy that still supports the feature. A foreground-only credential may use a when-unlocked policy, while a legitimate background refresh credential may require after-first-unlock access. Highly sensitive, device-bound material can justify a ThisDeviceOnly or passcode-required policy.
An audit maps each secret to its real access requirement instead of applying one Keychain wrapper default to every item. See Apple’s guide to restricting Keychain item accessibility.
2. Sensitive values escaping through logs
Development logs often accumulate tokens, email addresses, document text, model prompts, and server responses. Those calls can survive into distribution builds or appear in diagnostic material collected during support.
The audit should search every logging path—not only print()—and verify that private values use appropriate privacy annotations, release logging is intentional, and verbose network or AI diagnostics cannot expose user content.
Treat redaction as part of the logging API. Asking every caller to remember which fields are sensitive is not a durable control.
3. Secrets or sensitive state stored as preferences
UserDefaults is designed for preferences, not credentials, signing keys, or authorization decisions. Its backing representation is not a secret vault, even though files inside an app container benefit from platform data protection.
Use Keychain for small secrets. For sensitive structured data, document the protection requirements for the database and its journal or sidecar files. Core Data and SwiftData do not automatically provide application-level field encryption; if the threat model requires that, add and test it deliberately.
An audit also checks indirect leaks such as sensitive identifiers embedded in cache keys, filenames, widgets, shared defaults, or analytics properties.
4. App Transport Security exceptions that outlived development
App Transport Security (ATS) requires secure TLS connections by default. Development workarounds can leave broad exceptions such as NSAllowsArbitraryLoads in the production configuration.
Review every ATS exception, the domain it covers, the reason it exists, and whether a secure server configuration can replace it. Apple recommends relying on secure defaults and scoping unavoidable exceptions as narrowly as possible. See Preventing Insecure Network Connections.
Configuration-specific plists matter here: the Debug configuration can be safe while an old Release or extension target still carries a weaker policy.
5. Runtime-integrity controls that do not match the threat model
Jailbreak or tamper detection is not a universal requirement and cannot make a compromised client trustworthy. Simple file-path checks are easy to bypass, while aggressive detection can block legitimate users and create a false sense of security.
For apps with a documented high-risk threat model, an audit can assess runtime-integrity signals as defense in depth. The design must assume those signals can be bypassed and keep critical authorization, fraud controls, and server-side validation independent of them.
The useful finding is not “jailbreak detection missing.” It is whether the app has identified compromised-client risk and chosen proportionate controls.
6. Sensitive UI exposed in app-switcher or screen capture
When an app leaves the active state, the system may preserve a visual representation for transitions and the app switcher. Account numbers, health records, private messages, or authentication codes should not remain visible there.
Protect sensitive screens when the scene becomes inactive, and remove the protection only after the scene is active again. Test the result on a physical device; adding the cover after the scene has fully entered the background can be too late for the captured frame.
Also decide how the app responds to screen recording, mirroring, and screenshots. The correct behavior depends on the product and accessibility requirements—blanket blocking is not always appropriate.
7. Trust customization and pinning without an operational plan
ATS and correct system trust evaluation are the baseline for most apps. Certificate or public-key pinning can add defense in depth for a narrow high-risk threat model, but it also creates availability risk when certificates or keys rotate.
An audit inventories custom URLSession trust delegates and pinning libraries, then verifies hostname checking, failure behavior, backup pins, rotation ownership, and coverage across all sensitive endpoints.
The absence of pinning is not automatically a vulnerability. Improper trust overrides—such as accepting every challenge—are. Apple advises using the highest-level secure networking API that meets the requirement rather than replacing system trust casually.
8. Entitlements broader than the shipped feature set
Entitlements define capabilities granted to the signed app. Old App Groups, iCloud containers, HealthKit access, associated domains, or keychain groups may remain after the corresponding feature has changed.
Compare the entitlements embedded in the distribution archive—not only the source .entitlements file—with the current product behavior and App Store declarations. Repeat this for extensions, widgets, watch apps, and notification services because each target has its own boundary.
Remove unused capabilities and verify that required ones use the narrowest practical scope.
9. File protection assessed only on the primary database
Sensitive local data can exist in database journals, thumbnails, exports, temporary files, model inputs, and caches. Checking only the main SQLite file misses much of the real storage surface.
Choose Data Protection classes according to when the app genuinely needs access, then inspect the attributes of files produced at runtime. Background processing may require a different policy from foreground-only records, so “always use the strictest class” is not a complete design rule.
Include backups and shared containers in the review. A strongly protected database can still leak through an unprotected export or preview image.
10. Dependencies and privacy manifests reviewed separately
Privacy manifests describe required-reason API use and data practices, but correctness depends on the complete dependency graph. Adding or updating an SDK can change the app’s effective declarations without modifying first-party Swift code.
Inventory direct and transitive SDKs, verify required signatures and manifests where Apple requires them, and confirm the final PrivacyInfo.xcprivacy accurately describes the shipped app. Compare this with App Store Connect privacy answers rather than treating the files as unrelated paperwork.
Apple maintains the current requirements under Third-party SDK requirements and Describing data use in privacy manifests.
What a useful audit delivers
A useful finding connects evidence to a realistic scenario and a concrete remediation. It should include:
- the affected target, configuration, file, or runtime flow;
- the data or capability at risk;
- severity and likelihood in the product’s actual threat model;
- a reproducible verification step;
- a bounded fix and a way to confirm it worked;
- explicit notes for accepted risks or controls that are not applicable.
The result is a prioritized engineering plan, not a generic checklist or an automated scanner export.
A compact release checklist
- Map every credential to an explicit Keychain accessibility requirement.
- Search release logging paths for tokens, personal data, prompts, and payloads.
- Keep secrets out of preferences and review every local-data artifact.
- Remove or narrowly scope ATS exceptions.
- Treat runtime-integrity signals as optional defense in depth, not authorization.
- Protect sensitive UI before the scene becomes inactive.
- Review every custom trust handler and document any pin-rotation plan.
- Compare archive entitlements with shipped features and declarations.
- Inspect protection attributes for databases, sidecars, caches, and exports.
- Reconcile SDK inventory, privacy manifests, and App Store privacy answers.
For a broader implementation baseline, use the production iOS security guide. If the app also includes local models or Apple Foundation Models, review the on-device AI architecture audit findings and Core ML integration checklist.
Frequently asked questions
Does every app need a formal penetration test?
No. The assurance level should follow the data sensitivity, exposure, regulatory environment, and attacker model. A configuration and architecture audit may be proportionate for a local utility, while a finance or health service may also require API testing, mobile application security testing, and independent validation.
Is local-first architecture automatically secure?
No. Keeping data off a server removes a large remote exposure, but local files, backups, logs, shared containers, UI snapshots, and device compromise still matter. Local-first changes the threat model; it does not eliminate it.
Can static analysis find all ten issues?
Static tools can identify suspicious APIs and settings, but they cannot reliably determine business sensitivity, lifecycle timing, dependency behavior, operational key rotation, or whether a control matches the real threat model. Use automation to gather evidence, then review the evidence in product context.
Security is not a feature added at the end. It is the set of storage, network, capability, lifecycle, and dependency decisions that define what the released app can expose—and how confidently the team can prove otherwise.