Safeguard
Application Security•

How do you prevent SQL injection in a Swift app that uses SQLite?

Bind values with placeholders instead of string interpolation, allow list table and column names, and never build SQL from user text. Swift examples for SQLite and GRDB.

Safeguard Security Team
5 min read

To prevent SQL injection in Swift, never build SQL by interpolating user text into a string. Write the statement with placeholders, then bind values through the database API, so the engine always treats input as data. Names of tables and columns cannot be bound, so choose those from an allowlist you control.

Does a mobile app really have SQL injection?

Yes, though the attacker and the stakes differ from a web server. A local SQLite database inside an iOS or macOS app is usually reached by the user of that device, or by another app through a URL scheme, a share extension, a deep link or a document the app opens. If that input reaches a query as text, an injected clause can read other rows, change records the user should not touch, or crash the app. Apps that sync with a backend add another route: server data that ends up in a local query.

MITRE's CWE-89 describes the general weakness: SQL built from externally influenced input without neutralizing special elements, so that the query changes meaning.

What does the vulnerable code look like?

Constructed example: a search feature in a notes app.

let term = searchField.text ?? ""
let sql = "SELECT id, title FROM note WHERE title LIKE '%\(term)%'"
sqlite3_prepare_v2(db, sql, -1, &stmt, nil)

A term containing a quote ends the string literal, and what follows is parsed as SQL. Swift string interpolation is doing exactly what it was designed to do, and that is the problem: it knows nothing about SQL.

How do you fix it with the SQLite C API?

SQLite's documentation describes parameters in the statement text: a literal can be replaced by ?, ?NNN, :VVV, @VVV or $VVV, and values are set with the sqlite3_bind_* routines on the prepared statement.

let sql = "SELECT id, title FROM note WHERE title LIKE ?1"
sqlite3_prepare_v2(db, sql, -1, &stmt, nil)
let pattern = "%\(term)%"
sqlite3_bind_text(stmt, 1, pattern, -1, SQLITE_TRANSIENT)

Notice the wildcard percent signs are added to the bound value, not to the SQL. The statement text is now a constant. Whatever the user types is a string to compare.

How do you fix it with GRDB?

GRDB, a popular Swift SQLite toolkit, documents arguments for plain SQL and a safer interpolation form. Its README shows execute(sql:arguments:) with question mark placeholders, and a literal API where Swift interpolation generates bound parameters, with the comment "Avoid SQL injection with SQL interpolation". Its docs state that SQL interpolation produces valid SQL that does not suffer from SQL injection, and that plain strings remain available through the sql argument label.

try dbQueue.read { db in
    let rows = try Row.fetchAll(db,
        sql: "SELECT id, title FROM note WHERE title LIKE ?",
        arguments: ["%\(term)%"])
}

The rule to remember: if the string passed as sql: contains any user text, it is wrong. User text belongs in arguments:, or in an interpolated SQL literal.

What cannot be parameterized?

Placeholders stand in for values, not for identifiers or keywords. If a user can choose a sort column or a table, you cannot bind it. OWASP's SQL injection prevention cheat sheet lists allow list input validation for this case: map the user choice to a known name in your own code.

let allowed: [String: String] = ["date": "created_at", "title": "title"]
let column = allowed[userSort] ?? "created_at"
let sql = "SELECT id, title FROM note ORDER BY \(column)"

The interpolated value here is from your dictionary, never the raw input. OWASP also lists escaping user input as strongly discouraged.

What else should you check in review?

  • Search for sqlite3_prepare and execute(sql: calls where the string contains a Swift interpolation.
  • Look at LIKE queries, IN (...) lists built by joining strings, and dynamic ORDER BY.
  • Check data that arrives from deep links, pasteboard, files, and backend responses before it reaches a query.
  • Check migrations and debug helpers, which often build SQL from strings.

What to do

  1. Replace every interpolated SQL string with placeholders and bound values.
  2. With GRDB, pass user data via arguments: or interpolated SQL literals.
  3. Add wildcards to the bound value, not to the SQL.
  4. Allowlist any table, column or sort direction chosen by the user.
  5. Add a lint or code search rule that flags interpolation inside sql: strings.
  6. Test with a quote character, a semicolon and a long string in every search field.

Safeguard's SAST scanning can flag string built SQL in Swift code during review, and its dependency scanning covers the SQLite wrappers you pull in through Swift Package Manager.

Quick check

Does escaping single quotes make Swift string SQL safe?

No. OWASP lists escaping as strongly discouraged. Bind values with placeholders instead.

Can I bind a column name with a placeholder?

No. Placeholders are for values. Pick identifiers from a fixed allowlist in your code.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.