Mobile
WebView security: JavaScript bridges, file access and clearing data on logout
A WebView is a browser inside your app, but without the browser's protections. How to configure JavaScript bridges, file access, mixed content and URL allowlists.
· 10 min read

Many mobile apps have at least one WebView: a payment page, a help section, terms of use, part of the user account, or the whole app as a "wrapper" around a website. A WebView is convenient because it lets you reuse web code. But it is not a regular browser: there is no address bar, the user cannot see which site they are on, and most importantly, the web page can be connected to the app's native code through a bridge. That is why WebView settings are checked separately in the security review.
In this article we look at the main WebView risks, how to check them yourself and how to close them on Android, iOS and Flutter.
What the problem is
WebView risk comes from three things: what content is loaded, what that content is allowed to do, and where session data stays.
1. JavaScript bridges
addJavascriptInterface() on Android, WKScriptMessageHandler on iOS, a JavaScript channel in Flutter — all of these let JavaScript on a web page call the app's native functions. For example, Android.getToken() or window.webkit.messageHandlers.pay.postMessage(...).
If the WebView only opens your own pages, this is a managed risk. But if a foreign page ends up in the WebView (through a redirect, a deep link parameter, an ad, or XSS on the page), that page also gets access to the bridge and can read the token or trigger a native action. So in a WebView with a bridge, which pages can open must be strictly controlled.
2. File access
Android WebView has settings that allow the file:// scheme: setAllowFileAccess, setAllowFileAccessFromFileURLs, setAllowUniversalAccessFromFileURLs. If the last two are enabled, a page loaded via file:// can read other local files or any domain. This can leak the app's internal files (session, cache, database). On modern Android versions the defaults are safe, but code that turns them on manually still shows up.
3. Mixed content and certificate errors
If an https page loads resources over http (a script, an image, an iframe), that is mixed content. A script delivered over an open channel can be modified on the network. An even more dangerous mistake is calling handler.proceed() inside onReceivedSslError: this accepts any invalid certificate and effectively disables TLS. It is often written to work with a self-signed certificate on a test server and then left in the release build.
4. Uncontrolled navigation
Unless the app restricts it, a WebView opens any link on the page inside itself. As a result, the user is inside your app but on someone else's site (for example, a fake login page) and does not notice.
5. Data left after logout
A WebView keeps its own cookies, localStorage, IndexedDB and cache. When the user logs out, the native session is removed, but the web session cookie in the WebView stays. The next user (or whoever picks up the device) opens the WebView and lands in the previous account. When preparing our own app for the review, we closed exactly this point: WebView data is fully wiped on logout.
6. Debug mode
If WebView.setWebContentsDebuggingEnabled(true) (Android) or isInspectable = true (iOS) remains in the release build, anyone can attach to the WebView from a computer via Chrome DevTools or Safari Web Inspector and see the DOM, cookies and requests.
Why it matters for the security review
The OWASP MASVS PLATFORM group requires WebViews to be configured securely and bridges to be exposed only when necessary; MASTG has dedicated test cases for WebViews (JavaScript, file access, bridges, navigation). In the OWASP Mobile Top 10 (2024), these findings fall under M4 (Insufficient Input/Output Validation), M5 (Insecure Communication — mixed content and ignoring SSL errors), M8 (Security Misconfiguration) and M9 (Insecure Data Storage — data left after logout).
The expert decompiles the APK, searches the code for WebView settings, and during dynamic analysis tries to load a foreign URL into the WebView. So it pays to review this part in advance.
How to check it yourself
Static check
Open the APK with jadx and search:
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
Review each @JavascriptInterface method: what does it return and what does it do? A token, personal data or a payment action is a red flag.
In Flutter apps the main code lives in libapp.so, so search your source code instead: addJavaScriptChannel, setJavaScriptMode, loadRequest, NavigationDelegate.
On iOS, search the source for WKWebView, addScriptMessageHandler, allowFileAccessFromFileURLs, isInspectable. The deprecated UIWebView should not be there at all.
Dynamic check
- Try every path that opens a WebView: a deep link parameter, a push notification, a link on the page. A foreign domain must not open.
- Open
chrome://inspectwith the test device connected. The release app's WebView must not appear there. - Watch traffic through a proxy (for example, Burp Suite): there must be no
httprequests. Try without the proxy certificate installed — the WebView must not open the page (if it does, SSL errors are being ignored). - Log out, then open the WebView page again. Login must be required.
How to fix it
Android (Kotlin)
val allowedHosts = setOf("example.uz", "pay.example.uz")
webView.settings.apply {
javaScriptEnabled = true // only if needed
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) {
// foreign link: open in external browser or block
return true
}
return false
}
override fun onReceivedSslError(
view: WebView, handler: SslErrorHandler, error: SslError
) {
handler.cancel() // never proceed()
}
}
If you need to show local HTML files, use WebViewAssetLoader from the androidx.webkit library instead of file://: it serves files from an address like https://appassets.androidplatform.net/..., and you do not need to enable file access.
If you do need a JavaScript bridge:
- add it only when a trusted domain is loaded;
- keep the methods minimal and never return secrets;
- check the host of
webView.urlon every call; - where possible, use
addWebMessageListenerfromandroidx.webkit— it takes a list of allowed origins.
Clearing on logout:
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)
}
In the script message handler, check message.frameInfo.securityOrigin.host and accept messages only from allowed origins. Do not enable ATS (App Transport Security) exceptions such as NSAllowsArbitraryLoads.
On logout:
let store = WKWebsiteDataStore.default()
store.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
modifiedSince: .distantPast) { }
For sensitive pages you can also use WKWebsiteDataStore.nonPersistent() — then data is never written to disk.
Flutter (webview_flutter)
const allowedHosts = {'example.uz', 'pay.example.uz'};
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) // only if needed
..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'));
If you add a JavaScript channel, check the current page address when a message arrives (await controller.currentUrl()) and never return a token through the channel.
On logout:
Future<void> clearWebData(WebViewController? controller) async {
await WebViewCookieManager().clearCookies();
if (controller != null) {
await controller.clearCache();
await controller.clearLocalStorage();
}
}
On Android, cleartextTrafficPermitted="false" in network_security_config.xml also stops http requests in the WebView — we covered this in a separate article.
On the server side
Pages opened in a WebView must be protected like any web page: CSP, HttpOnly and Secure cookies, correct escaping against XSS. XSS on the page means access to the native bridge inside the WebView, so this is where web security and mobile security meet.
Short checklist
- The WebView opens only
httpsdomains from the allowlist; others go to the external browser or are blocked. -
onReceivedSslErroronly callscancel(); noproceed(). -
MIXED_CONTENT_NEVER_ALLOW, cleartext disabled, no ATS exceptions on iOS. - File access is disabled; local files are served via
WebViewAssetLoader. - JavaScript is enabled only in WebViews that need it.
- JavaScript bridges are minimal, return no secrets, and check the origin.
- WebView debugging and
isInspectableare disabled in release. - Cookies, localStorage, cache and history are cleared on logout.
-
UIWebViewis not used. - Pages opened in the WebView have CSP and XSS protection.
Conclusion
A WebView is a small browser inside your app, but without many of the browser's protections. The app must clearly define what gets loaded into it, what the loaded page can access, and what remains after the user logs out. An allowlist, disabled file access, strict TLS and a full wipe on logout close most WebView findings in a security review. Passing URLs into a WebView via deep links is covered in detail in deep links and exported components.
If you want to know before the review whether the WebViews in your app are configured correctly, request a security scan. The Claude AI agent reviews the code and settings, and our specialist verifies the findings on a device and helps you close them.


