Objective-C Static Analysis: Legacy iOS Code, Memory Management, and Why It Still Matters
In March 2015 a research team disclosed FREAK (CVE-2015-0204), a flaw in the OpenSSL and Apple Secure Transport TLS stacks that let an active network attacker downgrade an HTTPS handshake to a deliberately weakened 512-bit RSA export cipher and factor the resulting key in a few hours of EC2 time. The bug lived in cipher negotiation, but its blast radius lived inside Objective-C: every iOS app that linked Secure Transport through NSURLConnection or the older CFNetwork primitives inherited the downgrade until Apple shipped iOS 8.2 the same month. Banking apps, healthcare portals, and the long tail of in-house enterprise builds had to be repackaged and resubmitted. The post-mortem teaches the same lesson the Heartland-era payment integrators learned a decade earlier: when the network stack you bridge into is written in C, the safety guarantees of your high-level code stop at the framework boundary. A decade on, Objective-C is no longer the language Apple steers new projects toward, but it is still the language that holds the legacy iOS surface together — and an objective-c security scanner remains a load-bearing control in any mobile AppSec program.
Swift gets the new tutorials. Objective-C gets the existing balance sheet. Banking, insurance, healthcare, and government iOS apps written between 2008 and 2018 are still in production and still mixed line-by-line with Swift through the bridging header. Objective-C inherits every memory-safety pitfall of C — buffer overflows, format-string bugs, integer overflows, use-after-free — and stacks Cocoa-specific risks on top: retain/release cycles in legacy MRR code, insecure URL scheme handlers, NSURLConnection with NSAllowsArbitraryLoads, NSXMLParser external entity expansion, secrets in plist storage, method swizzling that breaks security invariants, and Keychain misuse. This guide walks the patterns an objective-c sast engine has to surface and the two vulnerable-to-fixed shapes that cover the most ground. The toolchain has been around for thirty years; the rules are mature, and there is no excuse for shipping these bugs to TestFlight.
Why Objective-C Still Matters in 2026
Apple positioned Swift as the future in 2014, but the migration curve in regulated industries is measured in decades. A core banking iOS application written in 2011 is not rewritten because the language changed; it is extended one screen at a time, with new Swift modules calling into the original Objective-C view controllers through the bridging header. The result is the codebase shape every mobile AppSec team inherits: a Swift surface that the type system protects, an Objective-C core that it does not, and a review that has to reason about both at once.
Objective-C's vulnerability landscape inherits the C floor and adds the Cocoa ceiling. C string functions — strcpy, sprintf, strcat, gets — still compile inside an .m file and still produce stack overflows. Format-string bugs surface every time NSLog or stringWithFormat: is handed a user-controlled value as the format argument. Integer overflows in NSData length arithmetic produce undersized allocations that the next memcpy overruns. Retain/release cycles persist in any module that predates ARC, and even in ARC code, __bridge casts to and from Core Foundation let lifetime bugs slip through. NSXMLParser with external entities enabled becomes an XXE primitive. NSURLConnection paired with NSAllowsArbitraryLoads downgrades transport security. CFPreferences and raw plist writes drop secrets into a backed-up sandbox file. Method swizzling becomes a security defect the moment a category retargets SecItemAdd or a network delegate.
Format String Vulnerability: NSLog Without a Specifier
The vulnerable shape is short, idiomatic, and ships in any logging helper that grew organically over five years of feature work. A view controller logs the search query the user typed and feeds it to NSLog as the format argument:
// VULNERABLE
#import <Foundation/Foundation.h>
@interface SearchLogger : NSObject
- (void)logQuery:(NSString *)query;
@end
@implementation SearchLogger
- (void)logQuery:(NSString *)query {
NSLog(query);
}
@endNSLog treats its first argument as a printf-style format string. Any % in the user-controlled value is interpreted as a conversion specifier, letting an attacker who reaches the logging path read adjacent stack memory with %x, dereference an arbitrary pointer with %s, or — on older runtimes — write to memory with %n. The bug catalogues as CWE-134. The fix is one specifier away:
// FIXED
#import <Foundation/Foundation.h>
@interface SearchLogger : NSObject
- (void)logQuery:(NSString *)query;
@end
@implementation SearchLogger
- (void)logQuery:(NSString *)query {
NSLog(@"%@", query);
}
@end With %@ driving the format, the user string is interpolated as a parameter and embedded conversion specifiers render literally. Clang's -Wformat-security catches the vulnerable shape at compile time, but most legacy iOS projects ship with the warning suppressed. SAST flags every NSLog, stringWithFormat:, initWithFormat:, and category-defined logger that takes a non-literal first argument, then traces the call sites back to any input source — request bodies, query parameters, UITextField text, push payloads — that can reach it.
NSXMLParser: External Entities Enabled by Default
The vulnerable construction parses an XML document received from a network response or read from an attachment the user selected, and leaves the parser at its defaults:
// VULNERABLE
#import <Foundation/Foundation.h>
@interface ConfigImporter : NSObject <NSXMLParserDelegate>
- (void)importFromData:(NSData *)data;
@end
@implementation ConfigImporter
- (void)importFromData:(NSData *)data {
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
parser.delegate = self;
parser.shouldResolveExternalEntities = YES;
[parser parse];
}
@end With shouldResolveExternalEntities set to YES, an XML payload that declares a DOCTYPE with an external entity pointing at file:///etc/passwd, the app's sandbox files, or an internal HTTP endpoint behind the device, can exfiltrate the contents into a parsed value the application later renders or transmits. Recursive entities produce billion-laughs amplification. CWE-611 catalogues the class. The fix tightens the parser surface:
// FIXED
#import <Foundation/Foundation.h>
@interface ConfigImporter : NSObject <NSXMLParserDelegate>
- (void)importFromData:(NSData *)data;
@end
@implementation ConfigImporter
- (void)importFromData:(NSData *)data {
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
parser.delegate = self;
parser.shouldResolveExternalEntities = NO;
parser.shouldProcessNamespaces = NO;
[parser parse];
}
@end With external entity resolution disabled, the parser refuses every SYSTEM or PUBLIC identifier and the document collapses to its inline content. Teams that need a stricter stack should drop to libxml2 directly with XML_PARSE_NOENT off and XML_PARSE_NONET on, or move to JSON where the schema allows. SAST flags every NSXMLParser instantiation that reaches a parse call without the explicit NO assignment.
Detection: Where Objective-C SAST Earns Its Keep
Objective-C analysis is mature because the toolchain has been around thirty years and Clang has shipped a first-party static analyzer for most of that time. Apple's scan-build wraps the Clang Static Analyzer for any Objective-C, C, and C++ translation unit and catches a useful baseline of memory, lifecycle, and API-misuse defects without any commercial license; running it against an Xcode project is a one-line invocation. Commercial Objective-C SAST engines — GraphNode, Checkmarx, Fortify, Veracode — add the inter-procedural data flow, the Cocoa rule packs, and the Info.plist configuration analysis that scan-build does not attempt. Mobile-specific platforms like MobSF complement source-level analysis with binary and runtime checks against the assembled IPA. The cheapest moment to fix an Objective-C bug is the keystroke that introduced it.
Prevention Checklist for Objective-C and Mixed iOS Codebases
Six rules close the overwhelming majority of real-world Objective-C vulnerabilities. They assume the project has been migrated to ARC and that the team has wired SAST into the pull-request gate.
- Pass user input through a format specifier, never as the format string. Forbid
NSLog(userInput)and[NSString stringWithFormat:userInput]. Enable-Wformat-securityand treat the warning as an error. - Disable external entities on every XML parser. Set
shouldResolveExternalEntitiestoNOon everyNSXMLParser; forlibxml2, leaveXML_PARSE_NOENToff and turn onXML_PARSE_NONET. - Replace C string and memory functions with bounds-checked equivalents. Substitute
strlcpyandstrlcatforstrcpyandstrcat, never callgets, and audit everymemcpywhose length comes from network arithmetic for integer overflow. - Move every credential into the Keychain with the strictest accessibility class. Forbid
CFPreferences, raw plist writes, and unencrypted SQLite for tokens, passwords, PII, and session state.kSecAttrAccessibleWhenUnlockedThisDeviceOnlyis the safe default. - Keep App Transport Security at its defaults and validate every URL scheme handler. Delete every
NSAllowsArbitraryLoadsentry fromInfo.plist, switchNSURLConnectioncallers toNSURLSession, and treat parameters reachingapplication:openURL:options:as taint sources. - Audit every method swizzle and every
__bridgecast. Swizzling a security-relevant selector is a backdoor; mismatched__bridge_retainedand__bridge_transfercasts leak Core Foundation objects or release them twice. Both need a reviewer and a SAST rule.
Where GraphNode SAST Fits
GraphNode SAST ships native Objective-C support alongside twelve other languages, with data flow tracking on the patterns this guide describes — user input reaching a format string, NSXMLParser instantiated without external entities disabled, C string functions across trust boundaries, secrets persisted to plist storage, and NSAllowsArbitraryLoads entries in Info.plist. The Xcode integration surfaces findings on the keystroke that introduced them, and CI gates the pull request before the IPA is signed. Mixed codebases get the same analysis on both languages — see the companion Swift static analysis guide and the SAST Tools Buyer's Guide.
Closing
Objective-C is no longer the language Apple steers new projects toward, but it holds the existing iOS balance sheet together, and the bugs it ships are the ones with thirty years of exploit research already published against them. The mixed Swift and Objective-C codebase is the realistic shape of any mature mobile estate: a typed Swift surface that the compiler protects, and an Objective-C core that it does not. Static analysis on the older code is a load-bearing control because the alternative is reading every diff by hand against a rule set the team can no longer keep in working memory. Wire the analyzer into the pull-request gate, treat the finding as a blocker, and the legacy core stops being the part of the codebase nobody wants to touch.
GraphNode SAST traces taint flow through Objective-C and mixed Swift codebases on the diff that introduced the bug — request a demo.
Request Demo