flawopen.com/SQL Injection/Kotlin
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. On Android, this shows up in SQLiteDatabase.rawQuery() calls built with Kotlin's string templates; on the server side (Ktor, Spring), the same JDBC-level rules apply as in Java, since Kotlin compiles to the same bytecode and calls the same APIs.
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
val query =
"SELECT * FROM users WHERE id = $userId"
val cursor = db.rawQuery(query, null)
// value bound, never templated in
val cursor = db.rawQuery(
"SELECT * FROM users WHERE id = ?",
arrayOf(userId)
)
The ? placeholder is resolved by SQLite's own bind mechanism, using the values in the second argument array — the query text is fixed before any value is attached to it. Kotlin's $userId string template can't provide this: it resolves before the string is ever handed to rawQuery(), so the value is already indistinguishable from the rest of the query.
"...$userId" is Kotlin's most natural, idiomatic way to build any string — exactly the same trap shape as Python's f-strings or JS template literals, just with Kotlin's own syntax.
The structured query() builder method takes selection arguments as a genuinely separate parameter, removing the temptation to build a WHERE clause by hand at all — it's a stronger default than remembering to parameterize rawQuery() correctly every time.
Kotlin's Exposed ORM parameterizes automatically through its typed DSL. Its exec() escape hatch for raw SQL carries the same risk as JDBC's raw Statement if the string passed to it was built with interpolation.
Null-safety and query-construction safety are unrelated properties — a non-null, well-typed String can still carry an injected SQL fragment.
A local SQLite database still parses and executes SQL exactly like a server-side one — an attacker who can influence the query (e.g. through a malicious Intent, a shared file, or synced data) has the same leverage as against a remote database.
grep -rn "rawQuery(" --include="*.kt" . | grep '\$'
grep -rn "\.exec(" --include="*.kt" . | grep '\$\|+'
Yes — Kotlin compiles to JVM bytecode and calls the same JDBC APIs as Java, so the same PreparedStatement-vs-Statement distinction from Java applies directly.
Room's @Query annotations with :param placeholders parameterize automatically — the same discipline applies if you ever drop to raw SupportSQLiteDatabase calls underneath it.