Безопасность Kotlin приложений: топ-10 уязвимостей на практике
Kotlin уверенно занимает позиции одного из самых популярных языков для разработки под Android и серверных приложений. Однако, как и любой другой язык, Kotlin не застрахован от уязвимостей. В этой статье мы разберем топ-10 наиболее критичных проблем безопасности, с которыми сталкиваются разработчики, и покажем, как их избежать на практике.
1. SQL-инъекции (SQL Injection)
Несмотря на то, что Kotlin часто используется с ORM-библиотеками, прямая работа с SQL-запросами остается распространенной практикой.
Уязвимый код:
fun getUser(id: String): User? {
val query = "SELECT * FROM users WHERE id = '$id'"
val result = connection.createStatement().executeQuery(query)
// ...
}
Безопасная версия (использование PreparedStatement):
fun getUserSafe(id: String): User? {
val query = "SELECT * FROM users WHERE id = ?"
val stmt = connection.prepareStatement(query)
stmt.setString(1, id)
val result = stmt.executeQuery()
// ...
}
Вывод: Никогда не конкатенируйте пользовательский ввод в SQL-запросы. Используйте параметризованные запросы или ORM (Exposed, Hibernate).
2. Межсайтовый скриптинг (XSS) в Web-приложениях
При разработке серверных приложений на Ktor или Spring Boot часто забывают экранировать вывод.
Уязвимый код (Ktor):
get("/user/{name}") {
val name = call.parameters["name"] ?: "Гость"
call.respondText("<h1>Привет, $name!</h1>", ContentType.Text.Html)
}
Безопасная версия:
import org.apache.commons.text.StringEscapeUtils
get("/user/{name}") {
val name = call.parameters["name"] ?: "Гость"
val safeName = StringEscapeUtils.escapeHtml4(name)
call.respondText("<h1>Привет, $safeName!</h1>", ContentType.Text.Html)
}
Совет: Всегда экранируйте пользовательский ввод перед выводом в HTML. Используйте шаблонизаторы с авто-экранированием (например, Kotlinx.html).
3. Небезопасное хранение паролей
Хранение паролей в открытом виде или с использованием слабых хешей — одна из самых частых ошибок.
Неправильный подход:
fun savePassword(password: String) {
val hash = password.md5() // Никогда так не делайте!
database.saveHash(hash)
}
Правильный подход (bcrypt):
import at.favre.lib.crypto.bcrypt.BCrypt
fun savePassword(password: String) {
val hash = BCrypt.withDefaults().hashToString(12, password.toCharArray())
database.saveHash(hash)
}
fun checkPassword(password: String, storedHash: String): Boolean {
return BCrypt.verifyer().verify(password.toCharArray(), storedHash).verified
}
Важно: Используйте bcrypt, scrypt или Argon2. Никогда не используйте MD5 или SHA-1 для хранения паролей.
4. Утечка конфиденциальных данных через логи
Разработчики часто логируют чувствительные данные, не проверяя, что попадает в вывод.
Проблемный код:
logger.info("Пользователь $userId авторизован с токеном: $token")
Безопасная версия:
logger.info("Пользователь $userId авторизован успешно")
// Или используйте маскирование:
fun maskToken(token: String): String =
token.take(4) + "****" + token.takeLast(4)
Правило: Никогда не логируйте пароли, токены, номера кредитных карт или персональные данные. Используйте библиотеки для маскирования (например, Logstash).
5. Небезопасная десериализация
Kotlin активно использует JSON-сериализацию. Неправильная настройка может привести к RCE-атакам.
Уязвимый код (Jackson):
val mapper = ObjectMapper()
val obj = mapper.readValue(userInput, Any::class.java) // Очень опасно!
Безопасная версия:
data class User(val name: String, val age: Int)
val mapper = ObjectMapper().apply {
// Отключаем автоматическую десериализацию в полиморфные типы
deactivateDefaultTyping()
}
val user = mapper.readValue(userInput, User::class.java)
Совет: Используйте kotlinx.serialization — он более безопасен по умолчанию и не поддерживает произвольную десериализацию.
6. Недостаточная проверка прав доступа (IDOR)
Уязвимость, когда пользователь может получить доступ к чужим данным, просто изменив ID в запросе.
Проблемный код (Ktor):
get("/api/user/{id}") {
val userId = call.parameters["id"]?.toIntOrNull() ?: return@get call.respondBadRequest()
val user = userRepository.findById(userId)
call.respond(user)
}
Безопасная версия:
get("/api/user/{id}") {
val userId = call.parameters["id"]?.toIntOrNull() ?: return@get call.respondBadRequest()
val currentUserId = getCurrentUserId() // Получаем ID текущего пользователя из сессии
if (currentUserId != userId && !isAdmin()) {
return@get call.respondForbidden()
}
val user = userRepository.findById(userId)
call.respond(user)
}
Правило: Всегда проверяйте, имеет ли текущий пользователь право на запрашиваемый ресурс.
7. Отсутствие валидации ввода
Даже при использовании type-safe подходов Kotlin, строки могут содержать опасные символы.
Пример:
fun processInput(input: String) {
// Пользователь может передать очень длинную строку (DoS-атака)
val result = someHeavyOperation(input)
}
// Безопасная версия:
fun processInputSafe(input: String) {
require(input.length <= 1000) { "Input too long" }
require(input.m