flawopen.com/sql-injection/Kotlin
SQL Injection in Kotlin (Android SQLite, Room, Exposed)
A Kotlin string template like "... = $id" inside rawQuery(), execSQL() or a Room raw query lets untrusted input rewrite your SQL and return every row (CWE-89). The bound form for Android SQLite, Room and Exposed, and how to sort by a user-chosen column safely.[1], [4], [2], [7], [10], [6]
Think of an Android access terminal with fixed commands and a badge-number slot. Kotlin string templates build the whole SQL command before SQLite sees it, so $userId can become part of the command itself. rawQuery() with selectionArgs, Room @Query parameters, and Exposed DSL expressions keep values in data slots. A SimpleSQLiteQuery needs its bindArgs too. execSQL() is for statements that do not return rows; use Android’s update() and delete() APIs for row changes. Column names are command choices, not values, so choose ORDER BY from fixed trusted options. A Kotlin when expression can select the ORDER BY column from fixed trusted options.[4], [2], [3], [10], [11]
Core Concepts & Subsystem Terms
String templates ($id) in SQL"... WHERE id = $userId"is ordinary string interpolation, evaluated before the database API is called. It looks close to Room's:nameplaceholders, but the value is already inside the SQL when the driver receives it.[1], [4]rawQuery(sql, selectionArgs)rawQuery(sql, selectionArgs)replaces each?with a bound value; Android documents these arguments as strings. SQLite applies column-affinity rules during comparisons, but binding is what prevents the value from becoming SQL syntax. Do not treat affinity as input validation.[2], [12]execSQL(sql, bindArgs)execSQL(sql)is for statements that do not return rows and is not the API for SELECT, INSERT, UPDATE, or DELETE. Android providesinsert(),update(), anddelete()for those operations;update()anddelete()accept a WHERE clause plus boundwhereArgs. TheexecSQL(sql, bindArgs)overload supports only byte arrays, strings, longs, and doubles for statements that are appropriate forexecSQL.[2]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Untrusted input reaches the repository
A deep link, intent extra, ContentProvider selection or backend request supplies userId, which is passed to findEmail() as a String.[4]
Source Code: Flaw vs. Secure Implementation
// UserRepository.kt: each query below interpolates input into the SQL text
class UserRepository(private val db: SQLiteDatabase) {
// rawQuery: the template runs first, and no selectionArgs are passed
fun findEmail(userId: String): String? =
db.rawQuery("SELECT email FROM users WHERE id = $userId", null).use { c ->
if (c.moveToFirst()) c.getString(0) else null
}
// Illustrative unsafe SQL construction is described in the prose; this read-only example avoids writes.
// ORDER BY column taken straight from the caller
fun listEmails(sortColumn: String): List<String> =
db.rawQuery("SELECT email FROM users ORDER BY $sortColumn", null).use { c ->
buildList { while (c.moveToNext()) add(c.getString(0)) }
}
}
@Dao
interface UserDao {
@RawQuery
fun search(query: SupportSQLiteQuery): List<User>
}
// Room: a raw query built from a template bypasses Room's binding
fun searchByName(dao: UserDao, name: String): List<User> =
dao.search(SimpleSQLiteQuery("SELECT * FROM users WHERE name = '$name'"))
// Exposed: exec() sends the interpolated SQL as written
fun countByTeam(team: String): Long? = transaction {
exec("SELECT count(*) FROM users WHERE team = '$team'") { rs ->
if (rs.next()) rs.getLong(1) else null
}
}
// UserRepository.kt: values are bound, columns come from a fixed mapping
class UserRepository(private val db: SQLiteDatabase) {
// rawQuery: ? marker, value passed in selectionArgs
fun findEmail(userId: Long): String? =
db.rawQuery("SELECT email FROM users WHERE id = ?", arrayOf(userId.toString())).use { c ->
if (c.moveToFirst()) c.getString(0) else null
}
// when maps the key to a constant column name
fun listEmails(sortKey: String): List<String> {
val sql = when (sortKey) {
"email" -> "SELECT email FROM users ORDER BY email"
"created" -> "SELECT email FROM users ORDER BY created_at"
else -> "SELECT email FROM users ORDER BY id"
}
return db.rawQuery(sql, null).use { c ->
buildList { while (c.moveToNext()) add(c.getString(0)) }
}
}
}
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE name = :name")
fun findByName(name: String): List<User>
}
// Room: :name is checked at compile time and bound as a parameter
fun searchByName(dao: UserDao, name: String): List<User> = dao.findByName(name)
// Exposed DSL: eq generates a bound parameter
fun countByTeam(team: String): Long = transaction {
Users.selectAll().where { Users.team eq team }.count()
}
Engineering & System Hardening Checklist
- ✓Add a detekt or Semgrep rule that flags input-derived interpolation or concatenation in SQL text passed to
rawQuery,execSQL,SimpleSQLiteQuery,query(selection)or Exposedexec; allow only reviewed fixed SQL fragments.[1], [2], [6] - ✓Pass every value through
selectionArgs,bindArgs,ContentValues, a Room@Queryparameter or an Exposed DSL expression.[2], [7], [10] - ✓Prefer Room
@Queryover@RawQuery; when@RawQueryis unavoidable, build it withSimpleSQLiteQuery(sql, bindArgs).[3], [8], [9] - ✓Map sort keys with a
whenexpression to constant column names; never interpolate a requested column into ORDER BY.[2], [1] - ✓Keep a ContentProvider non-exported unless sharing is required. If it must be shared, grant only the required read/write access and use narrowly scoped permissions or URI grants; verify the manifest configuration.[4], [13]
Sources
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- Android Developers — SQLiteDatabase API
- Android Developers — Access data using Room DAOs
- Android Developers — SQL injection
- JetBrains Exposed — Working with DSL
- OWASP SQL Injection Prevention Cheat Sheet
- Android Developers — Room @Query API
- Android Developers — Room @RawQuery API
- Android Developers — SimpleSQLiteQuery API
- JetBrains Exposed — Building SQL Statements
- JetBrains Exposed — Working with SQL Strings
- SQLite — Datatypes and Type Affinity
- Android Developers — <provider> manifest element