Введение
Kotlin — современный язык программирования, который активно используется для разработки Android-приложений, серверных решений (Ktor, Spring Boot) и кросс-платформенных проектов. Несмотря на его строгую типизацию и null-безопасность, разработчики часто допускают ошибки, приводящие к серьёзным уязвимостям. В этой статье мы разберём топ-10 уязвимостей Kotlin-приложений, основанных на стандартах OWASP, и покажем, как их избежать.
Статья будет полезна как начинающим, так и опытным Kotlin-разработчикам, желающим повысить уровень безопасности своих проектов.
1. SQL-инъекции (SQL Injection)
Проблема: Внедрение вредоносного SQL-кода через пользовательский ввод.
Пример уязвимого кода:
// Уязвимо! Не используйте конкатенацию строк
fun getUserById(id: String): User? {
val query = "SELECT * FROM users WHERE id = '$id'"
val result = connection.createStatement().executeQuery(query)
return result.toUser()
}
Решение: Используйте параметризованные запросы (PreparedStatement).
// Безопасно
fun getUserById(id: String): User? {
val query = "SELECT * FROM users WHERE id = ?"
val preparedStatement = connection.prepareStatement(query)
preparedStatement.setString(1, id)
val result = preparedStatement.executeQuery()
return result.toUser()
}
2. Межсайтовый скриптинг (XSS)
Проблема: Вставка вредоносного JavaScript-кода в веб-страницу.
Пример уязвимого кода (Ktor):
// Уязвимо! Не экранируйте вывод
get("/search") {
val query = call.parameters["q"] ?: ""
call.respondHtml {
h1 { +"Результаты поиска для: $query" } // XSS!
}
}
Решение: Используйте экранирование HTML (например, kotlinx.html с экранированием).
// Безопасно
import kotlinx.html.escapeHtml
get("/search") {
val query = call.parameters["q"] ?: ""
val safeQuery = query.escapeHtml()
call.respondHtml {
h1 { +"Результаты поиска для: $safeQuery" }
}
}
3. Небезопасное хранение паролей
Проблема: Хранение паролей в открытом виде или с использованием слабых хэшей (MD5, SHA-1).
Пример уязвимого кода:
// Уязвимо! MD5 — не подходит для паролей
fun saveUser(password: String) {
val hash = MessageDigest.getInstance("MD5").digest(password.toByteArray())
// сохраняем hash в БД
}
Решение: Используйте bcrypt, scrypt или Argon2.
// Безопасно (с использованием библиотеки jbcrypt)
import org.mindrot.jbcrypt.BCrypt
fun hashPassword(password: String): String {
return BCrypt.hashpw(password, BCrypt.gensalt(12))
}
fun checkPassword(password: String, hashed: String): Boolean {
return BCrypt.checkpw(password, hashed)
}
4. Утечка конфиденциальных данных через логи
Проблема: Логирование паролей, токенов, номеров карт.
Пример уязвимого кода:
// Уязвимо! Логируем чувствительные данные
fun login(username: String, password: String) {
log.info("Login attempt: username=$username, password=$password")
}
Решение: Используйте фильтры или маскирование.
// Безопасно
fun login(username: String, password: String) {
log.info("Login attempt: username=$username")
// password не логируем
}
5. Небезопасная десериализация
Проблема: Выполнение произвольного кода при десериализации непроверенных данных.
Пример уязвимого кода:
// Уязвимо! Десериализация без проверки
fun processData(data: ByteArray): Any {
val ois = ObjectInputStream(ByteArrayInputStream(data))
return ois.readObject() // может быть зловредным
}
Решение: Используйте безопасные форматы (JSON, Protocol Buffers) и проверяйте типы.
// Безопасно (используем kotlinx.serialization)
@Serializable
data class MyData(val name: String, val age: Int)
fun processData(json: String): MyData {
return Json.decodeFromString<MyData>(json)
}
6. Недостатки аутентификации и управления сессиями
Проблема: Слабые сессионные токены, отсутствие инвалидации сессий, хранение токенов в URL.
Пример уязвимого кода:
// Уязвимо! Токен в URL
val url = "https://api.example.com/user?sessionId=$sessionId"
Решение: Используйте безопасные HTTP-Only куки и генерируйте криптостойкие токены.
// Безопасно (используем SecureRandom)
import java.security.SecureRandom
fun generateSessionToken(): String {
val random = SecureRandom()
val bytes = ByteArray(32)
random.nextBytes(bytes)
return bytes.joinToString("") { "%02x".format(it) }
}
7. Переполнение буфера и целочисленное переполнение
Проблема: Kotlin работает на JVM, но нативные библиотеки (через JNI) могут быть уязвимы.
Пример уязвимого кода (JNI):
// Уязвимо! Нет проверки размера
fun processBuffer(data: ByteArray) {
val nativeBuffer = malloc(data.size) // может быть переполнение
// копирование данных
}
Решение: Всегда проверяйте границы массивов и используйте безопасные функции.
// Безопасно
fun safeProcessBuffer(data: ByteArray) {
require(data.size < MAX_BUFFER_SIZE) { "Buffer too large" }
val nativeBuffer = malloc(data.size) ?: throw OutOfMemoryError()
}