Predicate Injection

ID

swift.predicate_injection

Severity

high

Resource

Injection

Language

Swift

Tags

CWE:943, NIST.SP.800-53, OWASP:2021:A3

Description

Predicate injection occurs when user-controlled input is used to construct NSPredicate, NSComparisonPredicate, NSCompoundPredicate, or NSExpression format strings without proper validation or parameterization. This vulnerability allows attackers to manipulate the logic of predicates used for filtering data in Core Data queries, collection filtering, or other data operations.

Predicates in Swift/Objective-C use a format string syntax similar to SQL but for in-memory filtering. If user input is directly interpolated into predicate format strings, attackers can:

  • Bypass intended filtering logic

  • Access unauthorized data

  • Cause application crashes through malformed expressions

  • Perform denial of service attacks

The vulnerability is classified under CWE-943 (Improper Neutralization of Special Elements in Data Query Logic).

Rationale

NSPredicate and related classes (NSExpression, NSComparisonPredicate, NSCompoundPredicate) use a format string syntax that supports various operators and functions. When user input is directly incorporated into these format strings, attackers can inject malicious predicate expressions.

In Swift, a vulnerable code example might look like this:

import Foundation
import CoreData

func fetchUsers(context: NSManagedObjectContext, searchTerm: String) throws -> [User] {
    let fetchRequest = NSFetchRequest<User>(entityName: "User")

    // String interpolation of untrusted input into predicate
    let predicateString = "username == '\(searchTerm)'"

    // FLAW - Vulnerable to Predicate Injection
    fetchRequest.predicate = NSPredicate(format: predicateString)

    return try context.fetch(fetchRequest)
}

In this example, if searchTerm contains ' OR '1'=='1, the resulting predicate becomes:

username == '' OR '1'=='1'

This bypasses the intended filtering and returns all users instead of matching a specific username.

Another vulnerable pattern uses direct string concatenation:

func filterArray(items: [String], userInput: String) -> [String] {
    // FLAW: User input directly in predicate format string
    let predicate = NSPredicate(format: "SELF CONTAINS '\(userInput)'")
    return (items as NSArray).filtered(using: predicate) as! [String]
}

Even NSExpression can be vulnerable:

func evaluateExpression(fieldName: String, value: String) {
    // FLAW: User input in expression format
    let expression = NSExpression(format: "\(fieldName) == '\(value)'")
    // ... evaluation logic
}

Remediation

To prevent predicate injection, always use argument substitution with placeholders (%@, %d, etc.) instead of string interpolation or concatenation when incorporating user input into predicates.

Here is the revised, secure code example:

import Foundation
import CoreData

func fetchUsers(context: NSManagedObjectContext, searchTerm: String) throws -> [User] {
    let fetchRequest = NSFetchRequest<User>(entityName: "User")

    // FIXED: Use parameterized predicate with %@ placeholder
    fetchRequest.predicate = NSPredicate(format: "username == %@", searchTerm)

    return try context.fetch(fetchRequest)
}

The %@ placeholder ensures that searchTerm is treated as a literal string value, not as part of the predicate expression syntax.

For filtering collections:

func filterArray(items: [String], userInput: String) -> [String] {
    // FIXED: Use parameterized predicate
    let predicate = NSPredicate(format: "SELF CONTAINS %@", userInput)
    return (items as NSArray).filtered(using: predicate) as! [String]
}

When using argumentArray parameter:

func fetchUsersWithMultipleConditions(context: NSManagedObjectContext,
                                     name: String,
                                     minAge: Int) throws -> [User] {
    let fetchRequest = NSFetchRequest<User>(entityName: "User")

    // FIXED: Use argumentArray for multiple parameters
    let formatString = "name == %@ AND age >= %d"
    fetchRequest.predicate = NSPredicate(format: formatString,
                                        argumentArray: [name, minAge])

    return try context.fetch(fetchRequest)
}

For NSExpression, use argument arrays:

func evaluateExpression(value: String) {
    // FIXED: Use parameterized expression
    let expression = NSExpression(format: "name == %@", argumentArray: [value])
    // ... evaluation logic
}

Best practices for secure predicate usage:

  1. Always use parameterized predicates - Never use string interpolation or concatenation with user input

  2. Use argument substitution - Use %@ for objects, %d for integers, %f for floats, etc.

  3. Validate input - Even with parameterization, validate that user input meets expected format and constraints

  4. Use type-safe APIs - When possible, use Swift’s type-safe query builders provided by frameworks like Core Data

  5. Avoid dynamic field names - Don’t allow user input to determine field names in predicates

Example of comprehensive secure usage:

import Foundation
import CoreData

class SecureUserRepository {
    let context: NSManagedObjectContext

    init(context: NSManagedObjectContext) {
        self.context = context
    }

    func searchUsers(name: String?, email: String?, minAge: Int?) throws -> [User] {
        let fetchRequest = NSFetchRequest<User>(entityName: "User")

        var predicates: [NSPredicate] = []

        // Build predicates safely with parameterization
        if let name = name {
            // SAFE: Using %@ placeholder
            predicates.append(NSPredicate(format: "name CONTAINS[cd] %@", name))
        }

        if let email = email {
            // SAFE: Using %@ placeholder
            predicates.append(NSPredicate(format: "email == %@", email))
        }

        if let minAge = minAge {
            // SAFE: Using %d placeholder
            predicates.append(NSPredicate(format: "age >= %d", minAge))
        }

        if !predicates.isEmpty {
            // Combine predicates safely
            fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
        }

        return try context.fetch(fetchRequest)
    }
}