---
title: Dùng parameterized predicate trong Core Data - không ghép string người dùng
impact: CRITICAL
impactDescription: Dùng string interpolation trong NSPredicate format string cho phép attacker bypass filter, truy cập records không được phép hoặc crash app bằng cách inject ký tự đặc biệt.
tags: swift, ios, coredata, nspredicate, injection, parameterized-query, security
---

## Dùng parameterized predicate trong Core Data - không ghép string người dùng

`NSPredicate(format:)` xử lý %@ như placeholder và tự escape. Không bao giờ dùng string interpolation `\(variable)` trong format string của NSPredicate vì nó không được escape và dẫn đến injection.

**Incorrect (string interpolation trong predicate format):**

```swift
import CoreData

class MessageRepository {
    let context: NSManagedObjectContext

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

    // !! String interpolation - NSPredicate injection
    func searchMessages(keyword: String) -> [Message] {
        // Nếu keyword = "' OR '1'='1" thì trả về tất cả messages!
        let predicate = NSPredicate(format: "content CONTAINS '\(keyword)'")
        let request = Message.fetchRequest() as NSFetchRequest<Message>
        request.predicate = predicate
        return (try? context.fetch(request)) ?? []
    }

    // !! Concat string để build predicate
    func findByStatus(status: String) -> [Task] {
        let predicateStr = "status == '" + status + "'"  // Injection!
        let predicate = NSPredicate(format: predicateStr)
        let request = Task.fetchRequest() as NSFetchRequest<Task>
        request.predicate = predicate
        return (try? context.fetch(request)) ?? []
    }
}
```

**Correct (placeholder %@ với argumentArray):**

```swift
import CoreData

class MessageRepository {
    let context: NSManagedObjectContext

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

    // SAFE: %@ placeholder tự escape giá trị
    func searchMessages(keyword: String) -> [Message] {
        let predicate = NSPredicate(format: "content CONTAINS %@", keyword)
        let request = Message.fetchRequest() as NSFetchRequest<Message>
        request.predicate = predicate
        request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
        return (try? context.fetch(request)) ?? []
    }

    // SAFE: Multiple conditions dùng argumentArray
    func findMessages(from senderId: UUID, after date: Date) -> [Message] {
        let predicate = NSPredicate(
            format: "senderId == %@ AND createdAt > %@",
            argumentArray: [senderId as CVarArg, date as CVarArg]
        )
        let request = Message.fetchRequest() as NSFetchRequest<Message>
        request.predicate = predicate
        return (try? context.fetch(request)) ?? []
    }

    // SAFE: Enum status dùng Int value, không phải string từ user
    func findByStatus(_ status: TaskStatus) -> [Task] {
        let predicate = NSPredicate(format: "statusRaw == %d", status.rawValue)
        let request = Task.fetchRequest() as NSFetchRequest<Task>
        request.predicate = predicate
        return (try? context.fetch(request)) ?? []
    }
}
```

**Tools:** SwiftLint custom regex rule (detect `NSPredicate(format: ".*\\\(`), OWASP MASVS-CODE-4
