Externally Controlled Format String
ID |
swift.externally_controlled_format_string |
Severity |
low |
Resource |
Injection |
Language |
Swift |
Tags |
CWE:134, NIST.SP.800-53, PCI-DSS:6.5.1 |
Description
Format string vulnerabilities occur when user-controlled input is incorporated into format strings used by functions like String(format:), NSString.stringWithFormat:, NSLog, printf, and related functions. When an attacker can control the format string, they may be able to:
-
Read from arbitrary memory locations using format specifiers like
%@,%s,%p -
Cause application crashes through malformed format strings
-
Leak sensitive information from memory
-
In some cases (less common in Swift/Objective-C than in C), potentially write to memory
The vulnerability is particularly dangerous because format strings have a special syntax that allows reading and manipulating memory beyond the provided arguments.
Rationale
In Swift and Objective-C, format string functions interpret special sequences like %@, %d, %s, %p, etc. as placeholders for values to be inserted. When the format string itself comes from untrusted input, attackers can inject their own format specifiers to:
-
Information Disclosure: Use
%@or%sto read object descriptions or C strings from memory -
Memory Scanning: Use
%pto discover memory addresses (defeating ASLR) -
Crash/DoS: Provide mismatched format specifiers and arguments causing crashes
The following is an example of vulnerable code using string interpolation:
import Foundation
func logUserAction(action: String) {
// VULNERABLE: Using string interpolation to build format string
let formatString = "User performed action: \(action)"
// If action contains "%@", it will be interpreted as a format specifier
NSLog(formatString)
}
// Attacker can provide: action = "%@ %@ %@ %@"
// This will try to read objects from memory, potentially crashing or leaking data
Another vulnerable pattern uses string concatenation:
import Foundation
func displayMessage(userInput: String) {
// VULNERABLE: Building format string through concatenation
let format = "Message: " + userInput
let message = String(format: format)
print(message)
}
// If userInput is "%p %p %p", it will read pointers from stack
Direct use of variables as format strings:
import Foundation
func processTemplate(template: String) {
// VULNERABLE: Using untrusted input directly as format string
let result = String(format: template, "arg1", "arg2")
print(result)
}
// Attacker could provide template = "%@ %@ %@ %@ %@" to read beyond provided args
Using format functions with external input:
import Foundation
func logWithFormat(userMessage: String) {
// VULNERABLE: User message used as format string
NSLog(userMessage)
}
// If userMessage = "%@ %@ %@", this reads from stack
Remediation
To prevent format string vulnerabilities, never use untrusted input as a format string. Always use hardcoded format strings with proper argument substitution.
Here is the revised, secure code example:
import Foundation
func logUserAction(action: String) {
// FIXED: Hardcoded format string with %@ placeholder
NSLog("User performed action: %@", action)
}
For displaying messages:
import Foundation
func displayMessage(userInput: String) {
// FIXED: Hardcoded format string, user input as argument
let message = String(format: "Message: %@", userInput)
print(message)
}
For templates, validate and sanitize:
import Foundation
func processTemplate(template: String, args: [String]) {
// FIXED: Validate template is from trusted source
// Only use pre-defined, whitelisted templates
let allowedTemplates = [
"user_greeting": "Hello, %@!",
"item_count": "You have %d items",
"status_message": "Status: %@"
]
guard let safeFormat = allowedTemplates[template] else {
print("Invalid template")
return
}
// Use validated template with arguments
let result = String(format: safeFormat, arguments: args)
print(result)
}
For logging:
import Foundation
import os.log
func logWithFormat(userMessage: String) {
// FIXED: Hardcoded format string
let log = OSLog(subsystem: "com.example.app", category: "user_actions")
os_log("User message: %{public}@", log: log, type: .info, userMessage)
}
Best practices for secure format string usage:
-
Always use hardcoded format strings - Never interpolate, concatenate, or use variables as the format string itself
-
Use argument substitution - Use
%@for objects,%dfor integers,%ffor floats, etc., and pass values as separate arguments -
Prefer modern logging - Use
os_loginstead ofNSLogwhen possible, as it has better type safety -
Validate templates - If you must use dynamic format strings, maintain a whitelist of allowed templates
-
Avoid string interpolation in formats - Don’t use
\(variable)syntax in strings that will be used as format strings -
Sanitize user input - Even with proper formatting, validate and sanitize all user input
-
Use Swift String over NSString - Swift’s String type is generally safer and doesn’t have format string issues unless explicitly using format methods
Example of comprehensive secure usage:
import Foundation
import os.log
class SecureLogger {
private let log = OSLog(subsystem: "com.example.app", category: "general")
func logUserAction(username: String, action: String, timestamp: Date) {
// SAFE: All format specifiers are hardcoded
os_log(
"User %{public}@ performed %{public}@ at %{public}@",
log: log,
type: .info,
username,
action,
timestamp.description
)
}
func formatMessage(messageType: MessageType, content: String) -> String {
// SAFE: Format strings are hardcoded based on enum
let format: String
switch messageType {
case .error:
format = "ERROR: %@"
case .warning:
format = "WARNING: %@"
case .info:
format = "INFO: %@"
}
return String(format: format, content)
}
enum MessageType {
case error, warning, info
}
}
References
-
CWE-134: Use of Externally-Controlled Format String.