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:
-
Always use parameterized predicates - Never use string interpolation or concatenation with user input
-
Use argument substitution - Use
%@for objects,%dfor integers,%ffor floats, etc. -
Validate input - Even with parameterization, validate that user input meets expected format and constraints
-
Use type-safe APIs - When possible, use Swift’s type-safe query builders provided by frameworks like Core Data
-
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)
}
}