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:
-
NSKeyedUnarchiverdeserializes untrusted archives without proper class validation. -
JSONDecoderorPropertyListDecoderdecode data with dynamically determined types. -
JSONSerializationorPropertyListSerializationparse untrusted data returning Any type. -
Custom
Codableimplementations 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 byunsafe_reflectiondetector)
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
-
CWE-502 : Deserialization of Untrusted Data.
-
OWASP - Top 10 2021 Category A08 : Software and Data Integrity Failures.
-
Deserialization Cheat Sheet - OWASP Cheat Sheet Series.
-
Apple Developer - Archives and Serialization
-
NSKeyedUnarchiver Documentation - Note deprecation warnings
-
Using JSON with Custom Types - Example of serialization of custom types implementing the
Codableprotocol.