iOS Objective-C runtime and Swift bridging — source audit
TL;DR: Mixed Obj-C / Swift apps inherit Obj-C’s dynamic runtime: method swizzling, dynamic selectors,
objc_msgSendto arbitrary classes,class_addMethod, KVO key paths from user input. Each is a viable injection or RCE primitive in older codebases. Modern Swift can side-step most of this — unless it bridges via@objcfor backward compatibility. Companion to ios-source-review-methodology.
What to grep
1
2
3
4
5
grep -rn '@objc\|@objcMembers' .
grep -rn 'NSSelectorFromString\|performSelector\|class_addMethod\|method_exchangeImplementations' .
grep -rn 'method_swizzle\|objc_setAssociatedObject\|objc_getAssociatedObject' .
grep -rn 'NSClassFromString\|class_getMethod\|objc_msgSend' .
grep -rn 'valueForKey\|setValue\(_:forKey:\)\|valueForKeyPath\|setValue\(_:forKeyPath:\)' .
Pattern 1 — selector injection
1
2
3
4
SEL sel = NSSelectorFromString(userInput);
if ([self respondsToSelector:sel]) {
[self performSelector:sel withObject:arg]; // BAD: attacker picks the selector
}
If userInput flows from a URL parameter, deep link, or JSON, the attacker chooses which method runs. Even with respondsToSelector: gating, you’ve reduced the boundary to “any method on self” — usually still bad.
Swift equivalent:
1
2
let sel = NSSelectorFromString(rawSelector)
view.perform(sel, with: arg)
Pattern to flag: any Selector value derived from a string that came from outside.
Pattern 2 — KVC / KVO injection
1
object.setValue(json["value"], forKeyPath: json["key"] as! String)
setValue:forKeyPath: lets an attacker set arbitrary properties on the object — including ones the developer never intended exposing. Variant on mass assignment.
Trap: valueForKeyPath: from a string lets the caller traverse the object graph (children.first.password etc.).
Pattern 3 — method swizzling
1
2
3
Method orig = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad));
Method new = class_getInstanceMethod([self class], @selector(my_viewDidLoad));
method_exchangeImplementations(orig, new);
Swizzling itself isn’t a bug — it’s how many analytics SDKs work. But:
- Two SDKs swizzling the same selector → undefined order; bugs.
- A
+loadmethod that swizzles before the rest of the app initialises can break invariants. - Swizzles that bypass authentication or logging are a backdoor primitive — search for those.
Audit:
+loadand+initializeimplementations swizzling.- Multiple swizzles of the same selector.
- Swizzles of
-[NSURLSession dataTaskWithRequest:completionHandler:](TLS interception).
Pattern 4 — NSClassFromString instantiation
1
2
let cls = NSClassFromString(name) as? UIViewController.Type
cls?.init()
If name is user-controlled, the attacker picks the class — possibly one whose initialiser has side effects (filesystem writes, network calls, IPC).
Mitigation: switch on an allowlist of class names you intend to expose.
Pattern 5 — NSCoding / NSSecureCoding over the wire
1
2
3
4
let data = try Data(contentsOf: untrustedURL)
let archiver = try NSKeyedUnarchiver(forReadingFrom: data)
archiver.requiresSecureCoding = false // BAD
let obj = archiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey)
requiresSecureCoding = false permits any NSCoding class to decode — gadget chains identical in shape to Java/PHP deserialisation, just with iOS classes. Always use decodeObject(of:forKey:) with an explicit class allowlist.
1
grep -rn 'requiresSecureCoding\|decodeObject\(forKey:' .
Pattern 6 — performSelector with delay / on thread
1
view.perform(NSSelectorFromString(sel), with: arg, afterDelay: 0)
Delayed dispatch makes the selector run in a fresh runloop tick — code review and dynamic analysis miss the path. Same attacker-chosen-selector concern.
Pattern 7 — associated objects with user-controlled keys
1
objc_setAssociatedObject(self, (__bridge void *)key, value, OBJC_ASSOCIATION_RETAIN);
If key comes from user input, the attacker can collide with internal associations and silently overwrite them. Use stable static pointers as keys.
Swift specifics
Swift’s value types and strict typing make many of the above bugs less common — but if the API surface is exposed to Obj-C (subclassing NSObject, @objcMembers, optional protocol methods), the dynamic semantics return.
A clean Swift-only stack is much easier to reason about. A red flag: a “modern Swift app” with @objc annotations everywhere is operating under Obj-C’s runtime semantics, including the gotchas above.
Source-audit checklist
- No
performSelector:/NSSelectorFromStringfrom user input. - No
setValue:forKeyPath:from user input. - Method swizzling, if used, is documented and ordered (use
dispatch_onceand check for prior swizzles). - No
NSClassFromStringinstantiation of attacker-supplied class names. NSKeyedUnarchiveralways withrequiresSecureCoding = trueand explicit class allowlists.+load/+initializenot doing anything dangerous unconditionally.