Safeguard
Application Security•

How do you prevent SQL injection in Kotlin apps?

Kotlin string templates make SQL injection easy to write. Use bound parameters in JDBC, Room and Android queries, and an allow list for identifiers.

Safeguard Security Team
4 min read

Prevent SQL injection in Kotlin by never building a query with string templates or concatenation. Pass user values as bound parameters through PreparedStatement, Room's named arguments or the selection arguments of Android's query methods. For table and column names, which cannot be bound, check against a fixed allow list.

Why is Kotlin a special case?

Kotlin's string templates are pleasant to write and dangerous around SQL. The line "SELECT * FROM users WHERE name = '$name'" reads naturally and compiles without warning. The language has no concept of tainted input, so the compiler treats that query as an ordinary string.

OWASP lists prepared statements with parameterized queries as the first defense against SQL injection, and CWE-89 describes the weakness as building SQL from externally influenced input without neutralizing special elements. Kotlin sits on top of the same database drivers as Java, so the fixes are the same ones with Kotlin syntax.

What does the bug look like?

A constructed example for a server using JDBC:

// vulnerable
val sql = "SELECT id FROM accounts WHERE email = '$email' AND active = 1"
conn.createStatement().executeQuery(sql)

If email is supplied by a request, the attacker controls part of the SQL grammar, not just a value. The query's meaning can change: conditions can be bypassed, other tables read, and on some database configurations data modified.

How do you fix it with JDBC?

val sql = "SELECT id FROM accounts WHERE email = ? AND active = 1"
conn.prepareStatement(sql).use { ps ->
    ps.setString(1, email)
    ps.executeQuery().use { rs -> /* read rows */ }
}

The driver sends the statement structure and the value separately, so the value is never parsed as SQL. The same pattern applies to JdbcTemplate with ? placeholders and to query builders that bind values.

What about Room and Android?

Room's @Query annotation accepts named arguments such as :email that map to method parameters, and those are bound, not concatenated. Danger appears when you use a raw query object built from a string. With SupportSQLiteQuery or @RawQuery, pass arguments through the bind list, not the SQL text.

Android's SQLiteDatabase query and rawQuery methods take a selection string with ? placeholders and a separate array of selection arguments. Put the values in the array. Do not interpolate into the selection string, even in an app where the database is local, because content providers and exported components can pass attacker data into those queries.

What about ORMs and DSLs?

Kotlin SQL libraries like Exposed's DSL generate parameterized statements for values you pass through their operators. The risk returns at the escape hatches: raw SQL functions, string-based where fragments and custom expressions. Review each use. Search for $ inside any string that ends up in a query.

Which parts cannot be parameterized?

Identifiers and keywords: table names, column names, sort direction. A sort parameter such as ?sort=price is common. Map the input to a known value:

val column = when (sortParam) {
    "price" -> "price"
    "name" -> "name"
    else -> "id"
}

Then place column into the SQL. Never place the raw parameter there. Remember LIKE searches: bind the value, and escape % and _ if users should not control wildcards.

What to do

  1. Search for SQL keywords near $ templates and + concatenation, in code and tests.
  2. Replace with PreparedStatement, Room named arguments or selection argument arrays.
  3. Map sort and filter fields to allow-listed identifiers.
  4. Review every raw query escape hatch in your ORM or DSL.
  5. Give the application's database account only the rights it needs.
  6. Add tests that submit quotes and SQL fragments and expect them stored or matched literally.
  7. Run static analysis in CI and keep database drivers updated.

Safeguard's code scanning in the CLI and IDE extensions can flag string-built queries as you write them.

Quick check

Does escaping quotes prevent SQL injection?

Not reliably. Escaping rules vary by database and character set, and it is easy to miss a case. Bound parameters remove the problem by keeping data out of the SQL grammar.

Is a local Android database safe from injection?

Not necessarily. If an exported component or intent passes outside data into a query, injection is possible. Bind values there too.

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.