Приложение использует уязвимую compose-навигацию
Описание
Использование библиотеки Jetpack Compose Navigation, особенно версий до 2.7.0, с недостаточно защищённой или некорректной реализацией обработки Deep Links позволяет злоумышленникам или легальным пользователям напрямую обращаться к произвольным экранам приложения (Composable) и потенциально манипулировать их состоянием.
Compose Navigation позволяет разработчикам определять навигационные графы, где экраны (Composable) привязаны к уникальным маршрутам (route
) и могут быть вызваны извне приложения через Deep Links (navDeepLink
). Если приложение не выполняет надлежащую проверку и валидацию входящих Deep Link запросов и их параметров, это может привести к серьезным последствиям:
- Несанкционированный доступ: Обход экранов аутентификации/авторизации и прямой доступ к конфиденциальным разделам или функциям приложения (например, административным панелям, профилям других пользователей).
- Манипуляция состоянием: Передача неожиданных или вредоносных параметров через Deep Link, которые могут изменить поведение экрана или выполнить нежелательные действия.
- Обход бизнес-логики: Пропуск обязательных шагов или проверок, предусмотренных нормальным потоком навигации в приложении.
- Раскрытие информации: Доступ к экранам, отображающим чувствительные данные, которые не должны быть доступны без соответствующей авторизации.
Конкретные уязвимости (исправлены в версии 2.7.0 и выше):
- Переопределение аргументов по умолчанию через query-параметры: В версиях до
2.7.0-alpha01
злоумышленник мог передать query-параметр в Deep Link (myapp://user?id=admin
), который переопределял аргумент маршрута, даже если этот аргумент имел значение по умолчанию (route = "user?id={id}"
,defaultValue = "guest"
) и не был явно указан вuriPattern
дляnavDeepLink
. Это позволяло внедрять произвольные значения в аргументы, обходя предполагаемые безопасные значения по умолчанию. - Некорректная обработка несоответствия типов: В некоторых случаях, при передаче параметра через Deep Link с типом, не соответствующим ожидаемому
NavType
(например, строка вместо числа), компонент навигации мог использовать значение аргумента по умолчанию вместо того, чтобы вызвать ошибку или отменить навигацию. Это также могло привести к обходу проверок или непредсказуемому поведению.
Пример небезопасного экрана:
// Маршрут с аргументом userId, имеющим значение по умолчанию "guest"
composable(
route = "userProfile?userId={userId}",
arguments = listOf(navArgument("userId") {
type = NavType.StringType
defaultValue = "guest" // Предполагается безопасным значением по умолчанию
}),
// Deep Link НЕ определяет userId в uriPattern, но уязвимость позволяла его передать через query
deepLinks = listOf(navDeepLink { uriPattern = "[https://example.com/user](https://example.com/user)" })
) { backStackEntry ->
// В уязвимых версиях (до 2.7.0) вызов deep link
// "[https://example.com/user?userId=admin](https://example.com/user?userId=admin)" мог привести к тому,
// что userId здесь будет "admin", а не "guest".
val userId = backStackEntry.arguments?.getString("userId") ?: "guest"
// Если нет дополнительной проверки авторизации для userId,
// злоумышленник может получить доступ к чужому профилю.
UserProfileScreen(userId)
}
Рекомендации
Для устранения выявленного недостатка и повышения общей безопасности обработки Deep Links в Jetpack Compose Navigation рекомендуется предпринять следующие меры:
-
Обновить библиотеку Navigation Compose:
- Критически важно: Обновите зависимость
androidx.navigation:navigation-compose
до версии2.7.0
или выше. Это устранит известные уязвимости, связанные с переопределением аргументов по умолчанию и обработкой типов.
- Критически важно: Обновите зависимость
-
Реализовать строгую проверку доступа:
- Перед отображением любого экрана (Composable), доступного через Deep Link, всегда проверяйте статус аутентификации и авторизации пользователя. Не отображайте чувствительный контент или функции, если пользователь не имеет на это прав.
NavHost(navController, startDestination = "home") { composable( route = "sensitiveFeature/{id}", deepLinks = listOf(navDeepLink { uriPattern = "myapp://feature/{id}" }) ) { backStackEntry -> val featureId = backStackEntry.arguments?.getString("id") // Ключевая проверка: аутентифицирован ли пользователь И имеет ли он доступ к featureId? if (authViewModel.isUserAuthenticated() && permissionManager.canAccessFeature(featureId)) { SensitiveFeatureScreen(featureId) } else { // Перенаправить на логин или показать ошибку доступа navController.navigate("login_or_error") } } }
-
Валидировать все входящие параметры:
- Никогда не доверяйте параметрам из Deep Links (как из пути
/{param}
, так и из query?key=value
). - Явно извлекайте параметры из
backStackEntry.arguments
. - Проверяйте их на соответствие ожидаемому типу, формату, диапазону значений и бизнес-логике приложения.
- Особенно тщательно валидируйте параметры, которые используются для запросов к базе данных, сетевых вызовов или влияют на безопасность.
composable( route = "itemDetail/{itemId}", arguments = listOf(navArgument("itemId") { type = NavType.StringType }), // Используем String даже для ID для гибкости валидации deepLinks = listOf(navDeepLink { uriPattern = "[https://example.com/items/](https://example.com/items/){itemId}" }) ) { backStackEntry -> val rawItemId = backStackEntry.arguments?.getString("itemId") // Пример строгой валидации: не просто проверка на null, а проверка формата/существования if (isValidItemIdFormat(rawItemId) && dataRepository.itemExists(rawItemId)) { ItemDetailScreen(rawItemId!!) // Используем !! только после явной проверки } else { // Перенаправить на экран ошибки или главный экран Log.w("DeepLinkWarning", "Invalid or non-existent itemId received: $rawItemId") navController.navigate("error_or_home") } }
- Никогда не доверяйте параметрам из Deep Links (как из пути
-
Использовать промежуточные точки входа (при необходимости):
- Для сложных сценариев или особо чувствительных разделов можно использовать "промежуточный" Composable, который вызывается по Deep Link. Этот Composable выполняет все проверки безопасности и валидацию, а затем уже перенаправляет пользователя на целевой экран с проверенными данными.
-
Минимизировать использование Deep Links для чувствительных экранов:
- Оцените, действительно ли необходим прямой доступ через Deep Link к каждому экрану. Возможно, некоторые экраны должны быть доступны только через внутреннюю навигацию после аутентификации.
- Регулярно пересматривайте навигационный граф и
navDeepLink
определения.
Дополнительные рекомендации
- Мониторинг и журналирование: Логируйте попытки доступа через Deep Links, особенно неудачные или подозрительные. Это поможет выявлять попытки эксплуатации уязвимостей.
- Ограничение области действия: Если Deep Link предназначен только для определенных сценариев (например, открытие конкретного контента), убедитесь, что его
uriPattern
максимально специфичен и не допускает доступа к другим разделам. - Тестирование безопасности: Регулярно проводите тестирование на проникновение (pen-testing) и анализ кода (SAST/DAST) для выявления проблем с обработкой Deep Links.
- Следите за обновлениями: Поддерживайте все библиотеки Android Jetpack, включая Navigation, в актуальном состоянии.
Ссылки
- Jetpack Compose Navigation Bugs (fi5t.xyz) - Статья, описывающая конкретные уязвимости в версиях до 2.7.0.
- Официальная документация Android Developers - Navigation in Jetpack Compose.
- Официальная документация Android Developers - Создание Deep Link.
- Securing Deep Links in Android (Medium) - Общие принципы безопасности Deep Links.
- OWASP Mobile Security Testing Guide – Deep Link Testing (Раздел может немного отличаться в последней версии MSTG, но принцип тот же).