flawopen.com/SQL Injection/Swift

SQL Injection in Swift

Critical CWE-89 Draft — pending review
ELI5

Imagine a form that only ever expects a ticket number, like 482. SQL injection is what happens when someone types something sneaky into that box instead of a number — a trick phrase that makes the system say "show me every ticket" instead of just ticket 482, because the system never checked that what it received was actually just a number.

Key terms on this page
user-controlled input
Any value that ultimately came from whoever is using — or attacking — the app: a form field, a URL parameter, an uploaded filename, an HTTP header. The app can't assume it's well-formed or safe.
SQL query
The command sent to a database — e.g. "get this row," "delete this table." Its meaning comes entirely from its exact text, which is what makes injecting extra text into it dangerous.

What's happening

SQL injection happens when user-controlled input gets inserted directly into a database query's text. Swift most commonly talks to SQLite by bridging to its C API directly, or through a wrapper library — either way, Swift's own string interpolation (\(value)) is the same trap pattern seen in every other language's native interpolation feature.

Real-world impact

In 2015, UK telecom TalkTalk suffered a breach affecting over 150,000 customers after attackers exploited a SQL injection flaw in a legacy web page inherited through a company acquisition. The UK's data protection regulator fined TalkTalk £400,000, describing the failure as preventable and basic.

Source: UK Information Commissioner's Office enforcement notice, 2016 — see References below.

Vulnerable vs. fixed

VULNERABLE
// userId comes straight from user input
let query =
  "SELECT * FROM users WHERE id = \(userId)"
sqlite3_exec(db, query, nil, nil, nil)
FIXED
// value bound, never interpolated
var stmt: OpaquePointer?
sqlite3_prepare_v2(db,
  "SELECT * FROM users WHERE id = ?",
  -1, &stmt, nil)
sqlite3_bind_text(stmt, 1, userId, -1, nil)
sqlite3_step(stmt)

Why the fix works

sqlite3_prepare_v2 compiles the query's structure first; sqlite3_bind_text attaches the value to the ? placeholder afterward, at the SQLite protocol level — never as part of the parsed SQL text. Swift's \(userId) interpolation resolves before sqlite3_exec ever sees the string, so by then the value is indistinguishable from the rest of the query.

Swift-specific gotchas

Swift's bridging to the C SQLite API adds no safety layer by default

Calling sqlite3_exec from Swift behaves identically to calling it from C — the same sqlite3_exec vs. sqlite3_prepare_v2/sqlite3_bind_* distinction applies, and Swift's type safety elsewhere in the app doesn't extend into the bridged C call.

Higher-level wrappers (GRDB, FMDB) are safe only through their parameterized API

GRDB's execute(sql:arguments:) parameterizes automatically when you pass arguments separately. Passing a string already built with \(value) interpolation to the same call reintroduces the exact same risk the wrapper exists to prevent.

String interpolation syntax gives no visual warning

\(userId) looks identical whether it's embedding a hardcoded value or untrusted input — the risk is entirely in what the interpolated expression evaluates to, not in the syntax itself.

Common misconceptions

"Swift's strong type system protects me here"

A well-typed String can still contain an injected SQL fragment — type safety and query-construction safety are unrelated properties, the same as in Go, Rust, and every other statically-typed language.

"GRDB/FMDB means I don't have to think about this"

True only when using the library's parameterized query methods as intended — building the SQL string yourself before handing it to the wrapper bypasses its protection entirely.

How to check if you're affected

grep -rn "sqlite3_exec(" --include="*.swift" . | grep '\\\\(' grep -rn "execute(sql:" --include="*.swift" . | grep '\\\\('
No mainstream Swift linter (SwiftLint) currently ships a dedicated SQL-injection rule — a CI grep like the above is the practical check until a custom rule is added.

Prevention checklist

FAQ

Is this different on iOS vs. server-side Swift (Vapor)?

Same underlying principle — Vapor's Fluent ORM parameterizes its standard query API automatically; its raw-SQL escape hatch carries the same risk as any other language's raw-query method if built with interpolation.

Does Core Data have this problem?

Core Data's NSPredicate supports format strings with %@ substitution, which is safely bound — but a predicate built by directly interpolating a raw value into the format string bypasses that and reintroduces the risk.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: Command InjectionPath Traversal XSSInsecure Deserialization