Unsafe Reflection
ID |
swift.unsafe_reflection |
Severity |
high |
Resource |
Injection |
Language |
Swift |
Tags |
CWE:470, NIST.SP.800-53, OWASP:2021:A03, PCI-DSS:6.5.1 |
Description
Unsafe Reflection occurs when an application uses reflection in an insecure manner, potentially allowing an attacker to manipulate the class loader or execute arbitrary code. Reflection is powerful, but it must be used carefully to avoid introducing security vulnerabilities.
In Swift, iOS, and macOS applications, unsafe reflection commonly occurs when:
-
NSClassFromString()is used with user-controlled class names -
NSSelectorFromString()creates selectors from untrusted input -
Key-Value Coding (KVC) APIs access properties dynamically with user input
-
Objective-C runtime functions load classes or invoke methods based on external data
-
performSelector()variants execute methods determined at runtime
Rationale
Unsafe Reflection vulnerability (CWE: 470) arises when an application dynamically loads and executes classes determined at runtime without sufficient validation.
This can lead to unauthorized code execution, which represents a severe security threat. For example, if an attacker can influence the class name that a reflection call uses, they can potentially load malicious classes.
Swift and Objective-C provide some reflection capabilities that can be exploited if used with untrusted input. The following categories of unsafe reflection exist in Swift:
-
Class Loading - NSClassFromString() and Objective-C Runtime
Loading classes dynamically with user-controlled names allows attackers to instantiate arbitrary classes:
import Foundation
func loadUserClass(from request: URLRequest) {
guard let className = request.value(forHTTPHeaderField: "X-Class-Name") else {
return
}
// VULNERABLE: User-controlled class name
if let userClass = NSClassFromString(className) {
// Attacker can load any class available to the runtime
// including private classes with dangerous side effects
let instance = (userClass as! NSObject.Type).init()
processObject(instance)
}
}
An attacker could supply class names like:
- NSTask or Process to execute system commands
- Private framework classes with initialization side effects
- Classes that bypass sandbox restrictions
-
Selector Creation -
NSSelectorFromString()
Creating selectors from user input allows calling arbitrary methods:
import Foundation
func invokeMethod(on object: NSObject, methodName: String) {
// VULNERABLE: User-controlled selector
let selector = NSSelectorFromString(methodName)
if object.responds(to: selector) {
// Attacker can invoke any method the object responds to
object.perform(selector)
}
}
// Example attack
class ConfigManager: NSObject {
@objc func resetDatabase() {
// Dangerous operation
}
@objc func deleteAllData() {
// Destructive operation
}
}
func handleRequest(_ request: URLRequest) {
let config = ConfigManager()
let methodName = request.url?.queryParameters["action"] ?? ""
// FLAW: Attacker supplies "deleteAllData" as action
invokeMethod(on: config, methodName: methodName)
}
-
Key-Value Coding (KVC) - Dynamic Property Access
KVC allows accessing and modifying properties using string keys, which is dangerous with user input:
import Foundation
class UserSettings: NSObject {
@objc var username: String = ""
@objc var isAdmin: Bool = false
@objc var securityLevel: Int = 1
}
func updateSettings(from dict: [String: Any]) {
let settings = UserSettings()
// VULNERABLE: User-controlled keys in KVC
for (key, value) in dict {
// Attacker can set arbitrary properties including 'isAdmin'
settings.setValue(value, forKey: key)
}
saveSettings(settings)
}
// Attacker sends: {"isAdmin": true, "securityLevel": 999}
-
Key-Value Coding - Key Paths
Key paths enable nested property access, allowing deeper exploitation:
import Foundation
class Account: NSObject {
@objc var user: User = User()
@objc var settings: Settings = Settings()
}
class User: NSObject {
@objc var role: String = "user"
@objc var permissions: [String] = []
}
func applyConfiguration(_ keyPath: String, value: Any) {
let account = getCurrentAccount()
// VULNERABLE: User-controlled key path
// Attacker can use "user.role" to escalate privileges
account.setValue(value, forKeyPath: keyPath)
}
-
Objective-C Runtime API
Direct use of Objective-C runtime functions with user input:
import Foundation
import ObjectiveC
func loadClassFromInput(_ className: String) {
// VULNERABLE: objc_getClass with user input
if let clazz = objc_getClass(className) as? AnyClass {
let instance = (clazz as! NSObject.Type).init()
processInstance(instance)
}
}
func invokeMethodDynamically(target: AnyObject, selectorName: String) {
// VULNERABLE: sel_registerName with user input
let selector = sel_registerName(selectorName)
if let method = class_getInstanceMethod(type(of: target), selector) {
// Arbitrary method invocation
method_invoke(target, method)
}
}
-
Protocol Conformance Check with User Input
import Foundation
func checkProtocolConformance(_ protocolName: String, object: AnyObject) {
// VULNERABLE: Protocol name from user input
if let proto = NSProtocolFromString(protocolName) {
if object.conforms(to: proto) {
// Attacker controls which protocol to check
handleConformingObject(object, proto)
}
}
}
-
Undocumented Swift Runtime Functions
Undocumented Swift runtime functions like _typeByName() provide low-level type lookup:
import Foundation
func loadDynamicType(from url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let typeName = String(data: data, encoding: .utf8) ?? ""
// VULNERABLE: Undocumented _typeByName with user input
if let type = _typeByName(typeName) {
// Attacker can load arbitrary types including private/internal types
let instance = (type as? NSObject.Type)?.init()
processInstance(instance)
}
}
These functions are particularly dangerous because: - They are undocumented and unsupported - They can load private/internal types not accessible via public APIs - They bypass Swift’s type safety mechanisms - They may change or be removed in future Swift versions
-
Runtime Library Wrapper (wickwirew/Runtime)
Third-party libraries like Runtime provide convenient wrappers around reflection:
import Runtime
func updateObjectDynamically(object: inout Any, url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let properties = try JSONSerialization.jsonObject(with: data) as? [String: Any]
// VULNERABLE: Runtime library with user-controlled property names
for (key, value) in properties ?? [:] {
// Attacker can set arbitrary properties via Runtime library
try object.set(value, key: key)
}
}
func loadTypeViaRuntime(typeName: String) throws {
// VULNERABLE: Combining _typeByName with Runtime library
guard let type = _typeByName(typeName) else {
return
}
// Runtime library makes reflection easier but equally dangerous
let instance = try createInstance(of: type)
processInstance(instance)
}
While these libraries provide cleaner APIs than raw Objective-C runtime functions, they are equally vulnerable when used with untrusted input.
Remediation
To remediate unsafe reflection vulnerabilities, follow these guidelines:
-
Validate Input: Always validate the input strings used to form a reflective call. Employ whitelisting strategies to ensure only known and safe class names are used.
-
Least Privilege: Give the minimal permissions necessary for the reflection to operate. This decreases the impact of code execution should an unsafe reflection vulnerability be exploited.
-
Code Review and Testing: Perform thorough code reviews and security testing to identify potential insecure reflection points early in the development process.
-
Use Alternative Designs: Whenever possible, consider alternative designs that do not rely on reflection, which can often be less error-prone and more efficient.
In Swift, use these specific strategies to prevent unsafe reflection:
-
Option 1: Use Whitelist for Class Names
import Foundation
enum AllowedClass: String {
case userProfile = "MyApp.UserProfile"
case settings = "MyApp.Settings"
case preferences = "MyApp.Preferences"
var type: AnyClass? {
return NSClassFromString(self.rawValue)
}
}
func loadClassSafe(from input: String) -> AnyObject? {
// FIXED: Whitelist validation
guard let allowedClass = AllowedClass(rawValue: input),
let classType = allowedClass.type else {
logger.warning("Attempted to load disallowed class: \(input)")
return nil
}
return (classType as! NSObject.Type).init()
}
-
Option 2: Use Switch/Dictionary Instead of Dynamic Selectors
import Foundation
class ActionHandler {
enum Action {
case save
case load
case refresh
case export
}
func handleAction(_ action: Action) {
// FIXED: Use enum instead of string-based selector
switch action {
case .save:
save()
case .load:
load()
case .refresh:
refresh()
case .export:
export()
}
}
// Parse from string with validation
static func parseAction(_ string: String) -> Action? {
switch string.lowercased() {
case "save": return .save
case "load": return .load
case "refresh": return .refresh
case "export": return .export
default: return nil
}
}
}
func handleRequest(_ request: URLRequest) {
let handler = ActionHandler()
let actionString = request.url?.queryParameters["action"] ?? ""
// FIXED: Validate and map to enum
guard let action = ActionHandler.parseAction(actionString) else {
logger.warning("Invalid action: \(actionString)")
return
}
handler.handleAction(action)
}
-
Option 3: Whitelist KVC Keys
import Foundation
class UserSettings: NSObject {
@objc var username: String = ""
@objc var email: String = ""
@objc var theme: String = "light"
// Properties that should never be set via external input
@objc private var isAdmin: Bool = false
@objc private var securityLevel: Int = 1
static let allowedKeys: Set<String> = ["username", "email", "theme"]
func safeSetValue(_ value: Any?, forKey key: String) throws {
// FIXED: Whitelist validation
guard Self.allowedKeys.contains(key) else {
throw SettingsError.invalidKey(key)
}
setValue(value, forKey: key)
}
}
func updateSettings(from dict: [String: Any]) throws {
let settings = UserSettings()
// FIXED: Only allow whitelisted keys
for (key, value) in dict {
try settings.safeSetValue(value, forKey: key)
}
saveSettings(settings)
}
Option 4: Use Codable instead of KVC
import Foundation
struct UserSettings: Codable {
var username: String
var email: String
var theme: String
// These fields won't be decoded even if present in JSON
// (unless explicitly added to CodingKeys)
private var isAdmin: Bool = false
private var securityLevel: Int = 1
enum CodingKeys: String, CodingKey {
case username
case email
case theme
}
}
func updateSettings(from jsonData: Data) throws {
let decoder = JSONDecoder()
// FIXED: Type-safe decoding with explicit schema
let settings = try decoder.decode(UserSettings.self, from: jsonData)
saveSettings(settings)
}
-
Option 5: Avoid Objective-C Runtime APIs with External Input
import Foundation
// AVOID: Direct runtime API usage with user input
// Instead, use protocol-oriented programming
protocol Actionable {
func execute()
}
class SaveAction: Actionable {
func execute() { /* save logic */ }
}
class LoadAction: Actionable {
func execute() { /* load logic */ }
}
class ActionRegistry {
private var actions: [String: Actionable.Type] = [
"save": SaveAction.self,
"load": LoadAction.self
]
func getAction(named name: String) -> Actionable? {
// FIXED: Controlled registry lookup
return actions[name]?.init()
}
}
func handleAction(_ actionName: String) {
let registry = ActionRegistry()
// FIXED: Only registered actions can be instantiated
guard let action = registry.getAction(named: actionName) else {
logger.warning("Unknown action: \(actionName)")
return
}
action.execute()
}
Which reflection APIs should I avoid?
Never use:
-
Undocumented
_typeByName()and similar functions that call the Objective-C runtime.
Never use with untrusted input:
-
NSClassFromString()- Loads arbitrary classes -
NSSelectorFromString()- Creates arbitrary selectors -
NSProtocolFromString()- Loads arbitrary protocols -
objc_getClass()- Objective-C class lookup -
sel_registerName()- Selector registration -
setValue(_:forKey:)- KVC property setter -
value(forKey:)- KVC property getter -
setValue(_:forKeyPath:)- KVC key path setter -
perform(_:)- Dynamic method invocation -
objc_msgSend()- Direct message sending -
Runtime library
createInstance(of:)- Creates instances of user-controlled types -
Runtime library
set(_:key:)- Sets properties by user-controlled keys -
Runtime library
get(_:)- Gets properties by user-controlled keys -
Runtime library
property(named:)- Accesses properties by user-controlled names -
Runtime library
setProperties(_:)- Batch sets properties from user-controlled dictionary
Use with caution:
-
Mirror- Read-only reflection (safer, but can leak sensitive data) -
type(of:)- Safe for type inspection, but validate before using with runtime APIs
Preferred (safe):
-
Protocols and protocol-oriented programming
-
Enums for action dispatch
-
Codable for structured data
-
Closures/function pointers stored in dictionaries
-
Command pattern with whitelisted implementations
iOS/macOS Specific Considerations
App Sandbox Protection: - Sandbox limits class availability but doesn’t prevent all attacks - Private frameworks may still be accessible - URL schemes can deliver malicious class/selector names
Security Framework: - Even with entitlements, validate all reflection inputs - Keychain and secure enclave operations should never use dynamic selectors
Cross-Process Communication: - XPC and Distributed Objects can be exploited via reflection - Validate all inputs from other processes
Configuration
The detector has the following configurable parameters:
-
sources, that indicates the source kinds to check. -
neutralizations, that indicates the neutralization kinds to check.
Unless you need to change the default behavior, you typically do not need to configure this detector.
References
-
CWE-470 : Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')
-
Unsafe use of Reflection. in OWASP Vulnerabilities.
-
NSClassFromString and NSSelectorFromString Documentation.