Безопасность Kotlin приложений: топ-10 уязвимостей на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Безопасность 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                

Блоги

Рекомендаций по книгам