- Регистрация
- 23 Август 2023
- Сообщения
- 3 600
- Лучшие ответы
- 0
- Реакции
- 0
- Баллы
- 243
Offline
Intents: стандартный способ что-то сломать
Начну с того, что intents - это основа межпроцессного взаимодействия в Android. По сути, это сообщения-запросы, которые одно приложение отправляет системе или другому приложению. Представьте intent как конверт, в который вы кладёте информацию и отправляете либо другому экрану внутри вашего приложения, либо вообще другому приложению.
Простая аналогия: вы хотите открыть фотографию - создаёте intent с просьбой "покажи эту картинку", система смотрит: "О, есть приложение Галерея, оно умеет показывать картинки" и открывает его. Или вы хотите поделиться ссылкой - создаёте intent "поделиться текстом", система показывает список приложений (WhatsApp, Telegram, Email), которые могут это сделать.
Существует два типа intents:
Explicit (явный) - вы точно говорите: "Открой вот этот конкретный экран в моём приложении". Это как адресовать письмо конкретному человеку - относительно безопасно.
Implicit (неявный) - вы говорите: "Мне нужен кто-то, кто умеет открывать PDF" или "Мне нужно приложение для звонков". Система сама решает, кто подходит. Это как написать "Кому: любой врач" - откликнуться может кто угодно, и вот тут начинаются проблемы с безопасностью.
Здесь и далее в статье все примеры кода упрощены для наглядности. В production-коде потребуется больше обработки ошибок и edge cases.
// Explicit intent - относительно безопасно
val intent = Intent(this, TargetActivity::class.java)
intent.putExtra("user_id", 12345)startActivity(intent)
// Implicit intent - тут начинаются вопросы
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("myapp://profile/12345")
startActivity(intent)
Что может пойти не так? Любое приложение на устройстве может зарегистрировать intent filter для myapp:// схемы. Представьте: пользователь кликает на ссылку, а вместо вашего приложения открывается левое, которое просто собирает данные. В прошлом (относительно недавно) именно так взламывали банковские приложения - перехватывали deeplink'и с платёжной информацией.
Как защититься
Первое правило - никогда не передавайте чувствительные данные через implicit intents. Если всё-таки нужно, используйте App Links (Android 6.0+), которые требуют верификации домена.
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="myapp.com"
android:pathPrefix="/profile" />
</intent-filter>
Здесь autoVerify="true" заставляет Android проверить, что вы действительно владеете доменом myapp.com. Система скачает JSON-файл с вашего сервера и убедится, что вы имеете право обрабатывать эти ссылки. Да, это дополнительная настройка, но она отсекает 90% атак на deeplink.
Второе - всегда проверяйте входящие данные. Никогда не верьте тому, что приходит в intent.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userId = intent.getStringExtra("user_id")
// Плохо - слепо доверяем данным
loadUserProfile(userId)
// Хорошо - валидируем
if (userId != null && userId.matches(Regex("^[0-9]{1,10}$"))) {
loadUserProfile(userId)
} else {
// Логируем подозрительную активность
Timber.w("Invalid user_id received: $userId")
finish()
}
}
Я видел код, где разработчики передавали SQL-запросы через intent extras. Да-да, вы не ослышались. Результат был предсказуемый - SQL injection через обычный deeplink. Не повторяйте чужих ошибок.
URI schemes: когда браузер становится посредником
URI schemes - это способ запустить приложение из браузера или другого приложения по специальной ссылке типа myapp://action/params. В iOS это работает через Custom URL Schemes, в Android - через intent filters.
Главная проблема URI schemes - отсутствие проверки владельца схемы. Любое приложение может зарегистрировать тот же scheme, что и вы. На Android это превращается в диалог выбора приложения, который пользователь может случайно проигнорировать, выбрав не то, что нужно.
<!-- Это может зарегистрировать кто угодно -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
Классический сценарий атаки: вы делаете платёжное приложение с поддержкой URI scheme для инициации платежей. Злоумышленник публикует приложение с тем же scheme. Пользователь кликает на платёжную ссылку, выбирает не то приложение, и вуаля - его данные утекли.
Миграция на App Links
Я стараюсь избегать custom URI schemes там, где это возможно. Вместо этого использую HTTPS-ссылки с App Links. Разница кардинальная: система автоматически открывает ваше приложение без диалога выбора, потому что вы доказали владение доменом.
// Обработка App Link
val appLinkIntent = intentval
appLinkData: Uri? = appLinkIntent.data
appLinkData?.let { uri ->
val path = uri.path // /profile/12345
val params = uri.queryParameterNames // ?ref=email
// Парсим и обрабатываем
when {
path?.startsWith("/profile") == true -> {
val userId = path.removePrefix("/profile/")
openProfile(userId)
}
path?.startsWith("/payment") == true -> {
// Критично: проверяем подпись платежа на сервере
verifyAndProcessPayment(uri)
}
}
}
Что важно: даже с App Links нельзя слепо доверять параметрам. В примере выше с платежом я обязательно верифицирую данные на сервере. Злоумышленник может подделать ссылку (например, через фишинговый сайт), но серверная проверка подписи не даст выполнить платёж с изменёнными параметрами.
Защита параметров в URI
Если URI scheme всё-таки необходим (например, для совместимости со старыми версиями), добавьте криптографическую подпись.
// Генерация подписанной ссылки на сервере
fun generateSecureLink(action: String, params: Map<String, String>): String {
val timestamp = System.currentTimeMillis()
val data = "$action|${params.entries.joinToString("|")}|$timestamp"
val signature = HMAC_SHA256(data, SECRET_KEY)
return "myapp://$action?" +
params.entries.joinToString("&") { "${it.key}=${it.value}" } +
"×tamp=$timestamp&signature=$signature"}
// Проверка на клиенте
fun verifySecureLink(uri: Uri): Boolean {
val signature = uri.getQueryParameter("signature") ?: return false
val timestamp = uri.getQueryParameter("timestamp")?.toLongOrNull() ?: return false
// Проверяем актуальность (ссылка действует 5 минут)
if (System.currentTimeMillis() - timestamp > 300_000) {
return false }
// Проверяем подпись
val params = uri.queryParameterNames
.filter { it != "signature" && it != "timestamp" }
.sorted()
.joinToString("|") { "$it=${uri.getQueryParameter(it)}" }
val expectedSignature = HMAC_SHA256(params, SECRET_KEY)
return signature == expectedSignature
}
Да, это усложняет код, но зато делает перехват и модификацию ссылок бессмысленными. Без знания SECRET_KEY злоумышленник не сможет создать валидную подпись.
Shared Preferences: когда приватное становится публичным
Теперь о самом коварном механизме - Shared Preferences. По умолчанию они приватны для приложения, но Android позволяет делать их доступными другим приложениям через MODE_WORLD_READABLE и MODE_WORLD_WRITEABLE.
Хорошая новость: эти режимы deprecated с API 17 и полностью удалены в API 24. Плохая новость: я до сих пор встречаю код, который пытается их использовать, или хуже того - хранит чувствительные данные в обычных SharedPreferences, думая, что они защищены.
// Так делать НЕЛЬЗЯ
val prefs = getSharedPreferences("user_data", Context.MODE_PRIVATE)
prefs.edit()
.putString("password", password) // Храним пароль открытым текстом
.putString("credit_card", cardNumber)
.apply()
Shared Preferences хранятся в XML-файле в директории /data/data/package.name/shared_prefs/. На рутованных устройствах или через ADB backup любой желающий может прочитать эти файлы. Я как-то аудировал популярное финансовое приложение и нашёл токены доступа, хранящиеся в plaintext. Они даже не потрудились использовать EncryptedSharedPreferences.
EncryptedSharedPreferences: делаем правильно
С Android Security Library появился нормальный способ шифровать данные.
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
encryptedPrefs.edit()
.putString("auth_token", token)
.apply()
Здесь и ключи, и значения шифруются с использованием Android Keystore, который защищён на аппаратном уровне (если устройство поддерживает). Даже если кто-то получит доступ к файлу, расшифровать его без доступа к Keystore не получится.
Что важно: EncryptedSharedPreferences не решают всех проблем. На старых устройствах без hardware-backed Keystore или на эмуляторе защита слабее. Поэтому критически важные данные (платёжная информация, медицинские данные) лучше вообще не хранить на устройстве или использовать дополнительное шифрование на уровне приложения.
Content Providers для межпроцессного обмена
Если нужно безопасно передавать данные между своими приложениями (например, между основным приложением и виджетом), используйте Content Provider с правильными разрешениями.
class SecureDataProvider : ContentProvider() {
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
// Проверяем, что вызывающее приложение - это мы
val callingPackage = callingPackage
if (callingPackage != context?.packageName) {
throw SecurityException("Unauthorized access")
}
// Возвращаем данные
return null // Ваша логика
}
}
В манифесте:
<provider
android:name=".SecureDataProvider"
android:authorities="com.myapp.provider"
android:exported="true"
android:permission="com.myapp.permission.ACCESS_DATA"
/>
<permission
android:name="com.myapp.permission.ACCESS_DATA"
android:protectionLevel="signature"
/>
protectionLevel="signature" означает, что доступ получат только приложения, подписанные тем же ключом. Это идеальный вариант для обмена данными между вашими приложениями без риска утечки третьим сторонам.
Вместо заключения
Системы злоумышленников постоянно эволюционируют, появляются новые атаки и новые защиты. То, что работало в Android 8, может быть небезопасно в Android 14. Поэтому безопасность в Android - это не разовая настройка, а процесс.
Мой главный совет: читайте Android Security Bulletins, следите за CVE, связанными с вашими зависимостями, и регулярно проводите аудит своего кода.
И последнее: если сомневаетесь, стоит ли передавать какие-то данные между приложениями - не передавайте. Лучше лишний раз сходить на сервер, чем потом разгребать последствия утечки данных. Серьёзно, оно того не стоит.