Перейти к основному содержанию
Блог

Мобильные

Безопасность WebView: JavaScript-мосты, доступ к файлам и очистка при выходе

WebView — браузер внутри приложения, но без защит браузера. Как правильно настроить JavaScript-мосты, доступ к файлам, смешанный контент и allowlist URL.

· 10 мин. чтения

Фото: Ka Kit Pang, CC BY 3.0 · Wikimedia Commons

Во многих мобильных приложениях есть хотя бы один WebView: страница оплаты, раздел помощи, условия использования, часть личного кабинета или целое приложение-«обёртка» над сайтом. WebView удобен, потому что позволяет переиспользовать веб-код. Но это не обычный браузер: нет адресной строки, пользователь не видит, на каком сайте находится, и главное — веб-страница может быть связана с нативным кодом приложения через мост. Поэтому на экспертизе настройки WebView проверяются отдельно.

В этой статье разберём основные риски WebView, как проверить их самостоятельно и как закрыть на Android, iOS и во Flutter.

В чём проблема

Риск WebView складывается из трёх вещей: какой контент загружается, что этому контенту разрешено и где остаются данные сессии.

1. JavaScript-мосты

addJavascriptInterface() на Android, WKScriptMessageHandler на iOS, JavaScript channel во Flutter — всё это позволяет JavaScript на веб-странице вызывать нативные функции приложения. Например, Android.getToken() или window.webkit.messageHandlers.pay.postMessage(...).

Если WebView открывает только ваши страницы, это управляемый риск. Но если в WebView окажется чужая страница (через редирект, параметр deep link, рекламу, XSS на странице), она тоже получит доступ к мосту и прочитает токен или выполнит нативное действие. Поэтому в WebView с мостом нужно строго контролировать, какие страницы открываются.

2. Доступ к файлам

В Android WebView есть настройки, разрешающие схему file://: setAllowFileAccess, setAllowFileAccessFromFileURLs, setAllowUniversalAccessFromFileURLs. Если включены последние две, страница, загруженная через file://, может читать другие локальные файлы или любой домен. Это может привести к утечке внутренних файлов приложения (сессия, кеш, база данных). В современных версиях Android значения по умолчанию безопасны, но в коде встречается их ручное включение.

3. Смешанный контент и ошибки сертификата

Если на странице https загружаются ресурсы по http (скрипт, картинка, iframe), это смешанный контент (mixed content). Скрипт из открытого канала можно подменить в сети. Ещё опаснее ошибка — вызов handler.proceed() внутри onReceivedSslError: так принимается любой неверный сертификат, и TLS фактически отключается. Часто это пишут для работы с самоподписанным сертификатом тестового сервера, и код остаётся в release.

4. Неконтролируемая навигация

Если приложение не ограничивает навигацию, WebView открывает у себя любую ссылку со страницы. В итоге пользователь оказывается внутри вашего приложения, но на чужом сайте (например, на поддельной странице входа) и не замечает этого.

5. Данные, оставшиеся после выхода

WebView хранит свои cookie, localStorage, IndexedDB и кеш. При выходе из приложения нативная сессия удаляется, но cookie веб-сессии в WebView остаётся. Следующий пользователь (или человек, взявший устройство) открывает WebView и попадает в предыдущий аккаунт. Готовя к экспертизе наше собственное приложение, мы закрыли именно этот момент: при выходе данные WebView полностью очищаются.

6. Режим отладки

Если WebView.setWebContentsDebuggingEnabled(true) (Android) или isInspectable = true (iOS) остаётся в release-сборке, к WebView можно подключиться с компьютера через Chrome DevTools или Safari Web Inspector и увидеть DOM, cookie и запросы.

Почему это важно для экспертизы

Группа PLATFORM в OWASP MASVS требует, чтобы WebView были безопасно настроены, а мосты открывались только при необходимости; в MASTG есть отдельные тест-кейсы для WebView (JavaScript, доступ к файлам, мосты, навигация). По OWASP Mobile Top 10 (2024) эти находки попадают в M4 (Insufficient Input/Output Validation), M5 (Insecure Communication — смешанный контент и игнорирование ошибок SSL), M8 (Security Misconfiguration) и M9 (Insecure Data Storage — данные после выхода).

Эксперт декомпилирует APK, ищет настройки WebView в коде, а при динамическом анализе пытается загрузить в WebView чужой URL. Поэтому эту часть полезно просмотреть заранее.

Как проверить самостоятельно

Статическая проверка

Откройте APK в jadx и поищите:

jadx -d out app-release.apk
grep -rn "addJavascriptInterface\|setAllowFileAccess\|setAllowUniversalAccessFromFileURLs\|setAllowFileAccessFromFileURLs" out/sources
grep -rn "onReceivedSslError\|setMixedContentMode\|setWebContentsDebuggingEnabled" out/sources
grep -rn "@JavascriptInterface" out/sources

Просмотрите каждый метод с @JavascriptInterface: что он возвращает и что выполняет? Токен, персональные данные или платёжное действие — тревожный признак.

Во Flutter-приложениях основной код находится в libapp.so, поэтому ищите в исходниках: addJavaScriptChannel, setJavaScriptMode, loadRequest, NavigationDelegate.

На iOS ищите в исходном коде WKWebView, addScriptMessageHandler, allowFileAccessFromFileURLs, isInspectable. Устаревшего UIWebView быть не должно вовсе.

Динамическая проверка

  1. Проверьте каждый путь, по которому открывается WebView: параметр deep link, push-уведомление, ссылка на странице. Чужой домен открываться не должен.
  2. Откройте chrome://inspect при подключённом тестовом устройстве. WebView release-приложения там не должен появляться.
  3. Смотрите трафик через прокси (например, Burp Suite): запросов по http быть не должно. Попробуйте без установленного сертификата прокси — WebView не должен открывать страницу (если открывает, ошибка SSL игнорируется).
  4. Выйдите из аккаунта и снова откройте страницу WebView. Должен потребоваться вход.

Как закрыть

Android (Kotlin)

val allowedHosts = setOf("example.uz", "pay.example.uz")

webView.settings.apply {
    javaScriptEnabled = true            // только если нужно
    allowFileAccess = false
    allowContentAccess = false
    @Suppress("DEPRECATION")
    allowFileAccessFromFileURLs = false
    @Suppress("DEPRECATION")
    allowUniversalAccessFromFileURLs = false
    mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
    domStorageEnabled = true
}

WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)

webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(
        view: WebView, request: WebResourceRequest
    ): Boolean {
        val uri = request.url
        val ok = uri.scheme == "https" && uri.host in allowedHosts
        if (!ok) {
            // чужая ссылка открывается во внешнем браузере или блокируется
            return true
        }
        return false
    }

    override fun onReceivedSslError(
        view: WebView, handler: SslErrorHandler, error: SslError
    ) {
        handler.cancel()   // никогда не proceed()
    }
}

Если нужно показывать локальные HTML-файлы, вместо file:// используйте WebViewAssetLoader из библиотеки androidx.webkit: он отдаёт файлы по адресу вида https://appassets.androidplatform.net/..., и включать доступ к файлам не требуется.

Если JavaScript-мост нужен:

  • добавляйте его только при загрузке доверенного домена;
  • методы должны быть минимальными и не возвращать секреты;
  • при каждом вызове проверяйте host у webView.url;
  • по возможности используйте addWebMessageListener из androidx.webkit — он принимает список разрешённых origin.

Очистка при выходе:

fun clearWebData(context: Context, webView: WebView?) {
    CookieManager.getInstance().removeAllCookies(null)
    CookieManager.getInstance().flush()
    WebStorage.getInstance().deleteAllData()
    webView?.clearCache(true)
    webView?.clearHistory()
    webView?.clearFormData()
}

iOS (Swift)

let config = WKWebViewConfiguration()
config.preferences.javaScriptCanOpenWindowsAutomatically = false
let webView = WKWebView(frame: .zero, configuration: config)
#if DEBUG
if #available(iOS 16.4, *) { webView.isInspectable = true }
#endif

func webView(_ webView: WKWebView,
             decidePolicyFor action: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    guard let url = action.request.url,
          url.scheme == "https",
          let host = url.host,
          ["example.uz", "pay.example.uz"].contains(host) else {
        decisionHandler(.cancel); return
    }
    decisionHandler(.allow)
}

В script message handler проверяйте message.frameInfo.securityOrigin.host и принимайте сообщения только от разрешённых origin. Не включайте исключения ATS (App Transport Security), такие как NSAllowsArbitraryLoads.

При выходе:

let store = WKWebsiteDataStore.default()
store.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
                 modifiedSince: .distantPast) { }

Для чувствительных страниц можно использовать WKWebsiteDataStore.nonPersistent() — тогда данные не записываются на диск.

Flutter (webview_flutter)

const allowedHosts = {'example.uz', 'pay.example.uz'};

final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted) // только если нужно
  ..setNavigationDelegate(
    NavigationDelegate(
      onNavigationRequest: (request) {
        final uri = Uri.parse(request.url);
        final ok = uri.scheme == 'https' && allowedHosts.contains(uri.host);
        return ok ? NavigationDecision.navigate : NavigationDecision.prevent;
      },
    ),
  )
  ..loadRequest(Uri.parse('https://example.uz/help'));

Если добавляете JavaScript channel, при получении сообщения проверяйте адрес текущей страницы (await controller.currentUrl()) и никогда не возвращайте токен через channel.

При выходе:

Future<void> clearWebData(WebViewController? controller) async {
  await WebViewCookieManager().clearCookies();
  if (controller != null) {
    await controller.clearCache();
    await controller.clearLocalStorage();
  }
}

На Android cleartextTrafficPermitted="false" в network_security_config.xml тоже останавливает http-запросы в WebView — об этом мы писали в отдельной статье.

На стороне сервера

Страницы, открываемые в WebView, должны быть защищены как обычные веб-страницы: CSP, cookie с HttpOnly и Secure, корректное экранирование против XSS. XSS на странице означает доступ к нативному мосту внутри WebView, поэтому здесь веб-безопасность и мобильная безопасность сходятся.

Короткий чек-лист

  • WebView открывает только https-домены из allowlist; остальные — во внешнем браузере или блокируются.
  • В onReceivedSslError только cancel(); proceed() нет.
  • MIXED_CONTENT_NEVER_ALLOW, cleartext отключён, на iOS нет исключений ATS.
  • Доступ к файлам отключён; локальные файлы — через WebViewAssetLoader.
  • JavaScript включён только в тех WebView, где он нужен.
  • JavaScript-мосты минимальны, не возвращают секреты, origin проверяется.
  • В release отладка WebView и isInspectable отключены.
  • При выходе очищаются cookie, localStorage, кеш и история.
  • UIWebView не используется.
  • На страницах, открываемых в WebView, есть CSP и защита от XSS.

Вывод

WebView — маленький браузер внутри приложения, но без многих защит браузера. Приложение должно чётко определять, что в него загружается, к чему загруженная страница имеет доступ и что остаётся после выхода пользователя. Allowlist, отключённый доступ к файлам, строгий TLS и полная очистка при выходе закрывают большинство находок экспертизы по WebView. О передаче URL в WebView через deep link подробно написано в статье deep link и экспортированные компоненты.

Если хотите до экспертизы узнать, правильно ли настроены WebView в вашем приложении, оставьте заявку на сканирование. Claude AI агент просмотрит код и настройки, а наш специалист проверит находки на устройстве и поможет их закрыть.