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:

  1. 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.

  2. 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.

  3. Code Review and Testing: Perform thorough code reviews and security testing to identify potential insecure reflection points early in the development process.

  4. 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