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:

  1. Information Disclosure: Use %@ or %s to read object descriptions or C strings from memory

  2. Memory Scanning: Use %p to discover memory addresses (defeating ASLR)

  3. 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:

  1. Always use hardcoded format strings - Never interpolate, concatenate, or use variables as the format string itself

  2. Use argument substitution - Use %@ for objects, %d for integers, %f for floats, etc., and pass values as separate arguments

  3. Prefer modern logging - Use os_log instead of NSLog when possible, as it has better type safety

  4. Validate templates - If you must use dynamic format strings, maintain a whitelist of allowed templates

  5. Avoid string interpolation in formats - Don’t use \(variable) syntax in strings that will be used as format strings

  6. Sanitize user input - Even with proper formatting, validate and sanitize all user input

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

Configuration

This detector does not need any configuration.