Перейти к содержанию

Приложение использует уязвимую compose-навигацию

Описание

Использование библиотеки Jetpack Compose Navigation, особенно версий до 2.7.0, с недостаточно защищённой или некорректной реализацией обработки Deep Links позволяет злоумышленникам или легальным пользователям напрямую обращаться к произвольным экранам приложения (Composable) и потенциально манипулировать их состоянием.

Compose Navigation позволяет разработчикам определять навигационные графы, где экраны (Composable) привязаны к уникальным маршрутам (route) и могут быть вызваны извне приложения через Deep Links (navDeepLink). Если приложение не выполняет надлежащую проверку и валидацию входящих Deep Link запросов и их параметров, это может привести к серьезным последствиям:

  • Несанкционированный доступ: Обход экранов аутентификации/авторизации и прямой доступ к конфиденциальным разделам или функциям приложения (например, административным панелям, профилям других пользователей).
  • Манипуляция состоянием: Передача неожиданных или вредоносных параметров через Deep Link, которые могут изменить поведение экрана или выполнить нежелательные действия.
  • Обход бизнес-логики: Пропуск обязательных шагов или проверок, предусмотренных нормальным потоком навигации в приложении.
  • Раскрытие информации: Доступ к экранам, отображающим чувствительные данные, которые не должны быть доступны без соответствующей авторизации.

Конкретные уязвимости (исправлены в версии 2.7.0 и выше):

  1. Переопределение аргументов по умолчанию через query-параметры: В версиях до 2.7.0-alpha01 злоумышленник мог передать query-параметр в Deep Link (myapp://user?id=admin), который переопределял аргумент маршрута, даже если этот аргумент имел значение по умолчанию (route = "user?id={id}", defaultValue = "guest") и не был явно указан в uriPattern для navDeepLink. Это позволяло внедрять произвольные значения в аргументы, обходя предполагаемые безопасные значения по умолчанию.
  2. Некорректная обработка несоответствия типов: В некоторых случаях, при передаче параметра через 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 рекомендуется предпринять следующие меры:

  1. Обновить библиотеку Navigation Compose:

    • Критически важно: Обновите зависимость androidx.navigation:navigation-compose до версии 2.7.0 или выше. Это устранит известные уязвимости, связанные с переопределением аргументов по умолчанию и обработкой типов.
  2. Реализовать строгую проверку доступа:

    • Перед отображением любого экрана (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")
            }
        }
    }
    
  3. Валидировать все входящие параметры:

    • Никогда не доверяйте параметрам из 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")
        }
    }
    
  4. Использовать промежуточные точки входа (при необходимости):

    • Для сложных сценариев или особо чувствительных разделов можно использовать "промежуточный" Composable, который вызывается по Deep Link. Этот Composable выполняет все проверки безопасности и валидацию, а затем уже перенаправляет пользователя на целевой экран с проверенными данными.
  5. Минимизировать использование Deep Links для чувствительных экранов:

    • Оцените, действительно ли необходим прямой доступ через Deep Link к каждому экрану. Возможно, некоторые экраны должны быть доступны только через внутреннюю навигацию после аутентификации.
    • Регулярно пересматривайте навигационный граф и navDeepLink определения.

Дополнительные рекомендации

  • Мониторинг и журналирование: Логируйте попытки доступа через Deep Links, особенно неудачные или подозрительные. Это поможет выявлять попытки эксплуатации уязвимостей.
  • Ограничение области действия: Если Deep Link предназначен только для определенных сценариев (например, открытие конкретного контента), убедитесь, что его uriPattern максимально специфичен и не допускает доступа к другим разделам.
  • Тестирование безопасности: Регулярно проводите тестирование на проникновение (pen-testing) и анализ кода (SAST/DAST) для выявления проблем с обработкой Deep Links.
  • Следите за обновлениями: Поддерживайте все библиотеки Android Jetpack, включая Navigation, в актуальном состоянии.

Ссылки

  1. Jetpack Compose Navigation Bugs (fi5t.xyz) - Статья, описывающая конкретные уязвимости в версиях до 2.7.0.
  2. Официальная документация Android Developers - Navigation in Jetpack Compose.
  3. Официальная документация Android Developers - Создание Deep Link.
  4. Securing Deep Links in Android (Medium) - Общие принципы безопасности Deep Links.
  5. OWASP Mobile Security Testing Guide – Deep Link Testing (Раздел может немного отличаться в последней версии MSTG, но принцип тот же).
К началу