Code Injection During Deserialization

ID

swift.code_injection_deserialization

Severity

critical

Resource

Injection

Language

Swift

Tags

CWE:502, NIST.SP.800-53, OWASP:2021:A08, PCI-DSS:6.5.1

Description

Improper deserialization of untrusted data, possibly allowing code injection attacks.

Deserialization vulnerabilities arise when an application deserializes user-controlled data without proper validation, potentially allowing attackers to instantiate unexpected objects or execute crafted functions leading to arbitrary code execution or system exploits.

In iOS, macOS, and server-side Swift applications, unsafe deserialization commonly occurs when:

  • NSKeyedUnarchiver deserializes untrusted archives without proper class validation.

  • JSONDecoder or PropertyListDecoder decode data with dynamically determined types.

  • JSONSerialization or PropertyListSerialization parse untrusted data returning Any type.

  • Custom Codable implementations execute dangerous logic in init(from:) initializers.

Rationale

After detecting the vulnerable site, attackers typically craft a payload that is serialized and sent to the application. If the application deserializes this payload without validation, attackers can modify the expected data structure to escalate privileges or perform unwanted actions.

In the worst case, the attackers inject an object of an unexpected type, triggering chosen code execution. This could be used to exfiltrate internal server’s data, install malware e.g. to install and persist crypto-miners, or run a reverse shell.

Deserialization vulnerabilities in Swift allow attackers to execute arbitrary code by crafting malicious serialized objects. Swift’s serialization mechanisms can be exploited in several ways:


Case 1 - NSKeyedUnarchiver legacy unarchive (UNCONDITIONALLY UNSAFE):

The deprecated NSKeyedUnarchiver.unarchiveObject(with:) method is unconditionally unsafe:

import Foundation

func loadUserProfile(from url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)

    // VULNERABLE: Deprecated method with untrusted network data
    if let profile = NSKeyedUnarchiver.unarchiveObject(with: data) {
        // Attacker can craft archive to instantiate malicious objects
        // and execute arbitrary code during deserialization
        processProfile(profile)
    }
}

An attacker can create a malicious archive that: - Instantiates objects with side effects in their initializers - Triggers property observers that execute code - Manipulates object relationships to achieve code execution


Case 2 - JSONSerialization with weak type control:

JSONSerialization.jsonObject() returns Any type with weak type control:

import Foundation

func parseConfig(from url: URL) async throws {
    let (data, _) = try await URLSession.shared.data(from: url)

    // VULNERABLE: Returns Any type, weak type control
    if let json = try? JSONSerialization.jsonObject(with: data) {
        // Attacker controls the structure and types in the JSON
        // Can cause type confusion or unexpected behavior
        applyConfiguration(json)
    }
}

Case 3 - JSONDecoder with dynamic types:

When the decode type is determined dynamically, 'type confusion' attacks are possible:

import Foundation

func decodeWithDynamicType(data: Data, typeName: String) throws {
    // VULNERABLE: Type determined from user input
    guard let decodableType = NSClassFromString(typeName) as? Decodable.Type else {
        throw DeserializationError.invalidType
    }

    let decoder = JSONDecoder()
    // Type confusion: attacker controls which type is instantiated
    let obj = try decoder.decode(decodableType, from: data)
}

Case 4 - PropertyListSerialization with weak type control:

Similar to JSONSerialization, property list serialization with Any return type is vulnerable.

import Foundation

func loadSettings(from file: URL) throws {
    let data = try Data(contentsOf: file)

    // VULNERABLE: Returns Any, attacker controls plist structure
    var format = PropertyListSerialization.PropertyListFormat.xml
    let plist = try PropertyListSerialization.propertyList(
        from: data,
        options: [],
        format: &format
    )

    applySettings(plist)
}

Remediation

To prevent deserialization attacks in Swift, use modern type-safe decoders with fixed types and proper validation.


Option - NSKeyedUnarchiver with strict class whitelist:

If you must use NSKeyedUnarchiver, use the modern API with strict class whitelist:

import Foundation

func loadArchiveSafe(data: Data) throws -> [String: Any]? {
    // FIXED: Strict whitelist of allowed classes
    let allowedClasses: [AnyClass] = [
        NSDictionary.self,
        NSString.self,
        NSNumber.self,
        NSArray.self
    ]

    // Use modern API with class whitelist
    return try NSKeyedUnarchiver.unarchivedObject(
        ofClasses: allowedClasses,
        from: data
    ) as? [String: Any]
}

Option - Use JSONDecoder with fixed Codable Types:

import Foundation

struct UserProfile: Codable {
    let id: String
    let name: String
    let email: String
    // Only these specific fields will be decoded
}

func loadUserProfileSafe(from url: URL) async throws -> UserProfile {
    let (data, _) = try await URLSession.shared.data(from: url)

    // FIXED: Use JSONDecoder with fixed, compile-time type
    return try JSONDecoder().decode(UserProfile.self, from: data)
}

Option - Validate data before deserialization

import Foundation

struct APIResponse: Codable {
    let status: String
    let data: ResponseData
}

struct ResponseData: Codable {
    let userId: Int
    let items: [Item]
}

struct Item: Codable {
    let id: Int
    let name: String
}

func fetchAndDecodeSafe(from url: URL) async throws -> APIResponse {
    let (data, _) = try await URLSession.shared.data(from: url)

    // Validate data size
    guard data.count < 10_000_000 else {
        throw ValidationError.dataTooLarge
    }

    // Use schema validation if available
    let decoder = JSONDecoder()

    do {
        return try decoder.decode(APIResponse.self, from: data)
    } catch {
        // Log decoding failures for security monitoring
        logger.error("Failed to decode response: \(error)")
        throw ValidationError.invalidSchema
    }
}

Option 4: Custom decodable type with validation

import Foundation

struct SecureConfig: Codable {
    let endpoint: URL
    let timeout: TimeInterval
    let maxRetries: Int

    // Custom initializer with validation
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        // Decode and validate each field
        let endpointString = try container.decode(String.self, forKey: .endpoint)
        guard let url = URL(string: endpointString),
              url.scheme == "https" else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: [CodingKeys.endpoint],
                    debugDescription: "Invalid or insecure URL"
                )
            )
        }
        self.endpoint = url

        let timeout = try container.decode(TimeInterval.self, forKey: .timeout)
        guard timeout > 0 && timeout <= 300 else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: [CodingKeys.timeout],
                    debugDescription: "Timeout out of valid range"
                )
            )
        }
        self.timeout = timeout

        let retries = try container.decode(Int.self, forKey: .maxRetries)
        guard retries >= 0 && retries <= 5 else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: [CodingKeys.maxRetries],
                    debugDescription: "Retries out of valid range"
                )
            )
        }
        self.maxRetries = retries
    }
}

Summary: Which Deserialization APIs Should I Use?

Never use:

  • NSKeyedUnarchiver.unarchiveObject(with:) - Deprecated, unconditionally unsafe

  • NSKeyedUnarchiver.setClass(_:forClassName:) - Dangerous class substitution

Avoid with untrusted data:

  • JSONSerialization.jsonObject() - Returns Any, weak type control.

  • PropertyListSerialization.propertyList() - Returns Any, weak type control.

  • Dynamically determined types via NSClassFromString() (covered by unsafe_reflection detector)

Use with caution:

  • NSKeyedUnarchiver.unarchivedObject(ofClass:from:), for single class check.

  • NSKeyedUnarchiver.unarchivedObject(ofClasses:from:) with strict whitelist only.

Preferred (safe):

  • JSONDecoder.decode(\_:from:) with fixed Codable types.

  • PropertyListDecoder.decode(_:from:) with fixed Codable types.

  • Custom Codable implementations with validation.

  • Schema validation frameworks.

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