flawopen.com/SQL Injection/Swift
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.
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.
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.// userId comes straight from user input
let query =
"SELECT * FROM users WHERE id = \(userId)"
sqlite3_exec(db, query, nil, nil, nil)
// 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)
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.
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.
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.
\(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.
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.
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.
grep -rn "sqlite3_exec(" --include="*.swift" . | grep '\\\\('
grep -rn "execute(sql:" --include="*.swift" . | grep '\\\\('
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.
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.