How to Build a Secure Android WebView Wrapper with Kotlin
A practical guide to building a security-focused, reusable WebView component for Android using Kotlin and Jetpack Compose.
WebView is extremely useful when an Android application needs to display web content inside the app. But WebView also exposes powerful browser capabilities, so an insecure configuration can increase the application's attack surface.
In this guide, we'll build a secure Android WebView wrapper with a clear navigation policy and security-focused defaults.
We'll cover:
- HTTPS-only navigation
- Trusted URL allowlisting
- Safe Browsing
- JavaScript controls
- File and content access restrictions
- Secure JavaScript bridges
- SSL error handling
- External navigation
- Deep-link validation
- Jetpack Compose integration
- Production security testing
Why WebView Security Matters
A WebView can:
- Execute JavaScript
- Store cookies and web data
- Navigate between URLs
- Access web storage
- Download content
- Communicate with native Android code
Because of these capabilities, this should not be treated as a simple UI component:
webView.loadUrl(url)
If url comes from an intent, deep link, notification, API response, or user-controlled source, it should be treated as untrusted input.
A secure WebView should follow a simple principle:
Receive URL
↓
Validate URL
↓
Check HTTPS
↓
Check trusted host
↓
Apply navigation policy
↓
Load content
1. Define Trusted Domains
Start with an explicit allowlist.
private val allowedHosts = setOf(
"example.com",
"www.example.com"
)
Never rely on simple string prefix matching:
url.startsWith("https://example.com")
A URL such as:
https://example.com.attacker.com
could pass a naive prefix check.
Instead, parse the URL and compare the actual host.
fun isTrustedUrl(
url: String,
allowedHosts: Set<String>
): Boolean {
return try {
val uri = Uri.parse(url)
uri.scheme.equals("https", ignoreCase = true) &&
uri.host?.lowercase() in allowedHosts
} catch (e: Exception) {
false
}
}
This makes the security policy explicit and easier to test.
2. Create a Secure WebViewClient
The WebViewClient should control navigation.
class SecureWebViewClient(
private val allowedHosts: Set<String>
) : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val uri = request?.url ?: return true
val isHttps =
uri.scheme.equals("https", ignoreCase = true)
val host = uri.host?.lowercase()
val isTrustedHost =
host != null && host in allowedHosts
return !(isHttps && isTrustedHost)
}
}
The basic policy becomes:
HTTPS + trusted host
↓
Allow
Everything else
↓
Block / handle externally
You can extend this policy later for authentication, payments, downloads, or external browser navigation.
3. Configure WebView Securely
Create a dedicated configuration instead of scattering settings throughout the application.
fun configureSecureWebView(
webView: WebView
) {
webView.settings.apply {
javaScriptEnabled = false
allowFileAccess = false
allowContentAccess = false
databaseEnabled = false
setSupportMultipleWindows(false)
builtInZoomControls = false
displayZoomControls = false
safeBrowsingEnabled = true
}
}
The important rule is:
Do not enable a WebView capability unless your application actually needs it.
4. JavaScript: Enable Only When Required
Some modern websites require JavaScript.
If your trusted website requires it:
webView.settings.javaScriptEnabled = true
But JavaScript increases the attack surface.
When JavaScript is enabled:
- Restrict navigation.
- Do not load arbitrary domains.
- Minimize JavaScript bridges.
- Validate data crossing the WebView/native boundary.
- Avoid exposing sensitive Android functionality.
5. Secure JavaScript Bridges
addJavascriptInterface() can expose Android functionality to JavaScript.
For example:
class SecureBridge {
@JavascriptInterface
fun getAppVersion(): String {
return BuildConfig.VERSION_NAME
}
}
Register it only when necessary:
webView.addJavascriptInterface(
SecureBridge(),
"AndroidBridge"
)
Keep the exposed API:
- Small
- Explicit
- Non-sensitive
- Easy to audit
Avoid exposing powerful operations such as arbitrary file access, command execution, account operations, or sensitive application APIs through a JavaScript bridge.
6. Restrict File and Content Access
If local file loading is not required:
webView.settings.allowFileAccess = false
webView.settings.allowContentAccess = false
This reduces unnecessary access to local resources.
Do not enable these settings simply because a tutorial or sample project enables them.
7. Enable Safe Browsing
Where supported by the Android/WebView environment:
webView.settings.safeBrowsingEnabled = true
Safe Browsing should be considered an additional security layer, not a replacement for URL validation.
A stronger model is:
Trusted URL Policy
+
HTTPS
+
Safe Browsing
+
Restricted WebView Settings
8. Never Ignore SSL Errors
One of the most dangerous WebView patterns is:
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.proceed()
}
Do not blindly proceed after certificate errors.
A safer default is:
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.cancel()
}
If your organization uses a private CA or custom certificate infrastructure, configure trust correctly instead of bypassing TLS validation.
9. Handle WebView Errors
A production wrapper should expose loading failures to the application UI.
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
if (request?.isForMainFrame == true) {
// Update the application error state.
}
}
This lets your Compose screen display a proper error state instead of leaving the user with a blank page.
10. Build the Compose WebView Wrapper
Now we can integrate the secure WebView with Jetpack Compose.
@Composable
fun SecureWebView(
url: String,
allowedHosts: Set<String>,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = false
allowFileAccess = false
allowContentAccess = false
safeBrowsingEnabled = true
setSupportMultipleWindows(false)
}
webViewClient = SecureWebViewClient(
allowedHosts = allowedHosts
)
if (isTrustedUrl(url, allowedHosts)) {
loadUrl(url)
}
}
},
update = { webView ->
if (
isTrustedUrl(url, allowedHosts) &&
webView.url != url
) {
webView.loadUrl(url)
}
}
)
}
11. Use the Wrapper From a Compose Screen
@Composable
fun ArticleScreen() {
val allowedHosts = remember {
setOf(
"example.com",
"www.example.com"
)
}
SecureWebView(
url = "https://example.com/article",
allowedHosts = allowedHosts,
modifier = Modifier.fillMaxSize()
)
}
Now individual screens do not need to implement their own WebView security policy.
12. Validate Deep Links and Intent URLs
Never directly load a URL received from an Android intent.
Avoid:
val url = intent.getStringExtra("url")
webView.loadUrl(url)
Instead:
val url = intent.getStringExtra("url")
if (url != null && isTrustedUrl(url, allowedHosts)) {
webView.loadUrl(url)
}
The same principle applies to URLs received from:
- Push notifications
- Deep links
- API responses
- QR codes
- User input
- External applications
13. Control External Navigation
Your application may need to open external websites in Chrome or another browser.
A common policy is:
Trusted domain
↓
Open inside WebView
Unknown domain
↓
Do not load inside WebView
↓
Optionally handle externally
The exact behavior should depend on your product requirements and security model.
14. Cookies and Authentication
If the website uses authentication cookies, do not disable cookies blindly.
Instead, understand:
- Which domains receive cookies
- Which cookies contain authentication state
- Whether HTTPS is enforced
- Whether cookies use
Secure - Whether sensitive cookies use
HttpOnly - Whether
SameSiteis configured appropriately
WebView security depends on both the Android client and the server.
15. WebView Security Is a Full-Stack Responsibility
A secure Android wrapper cannot compensate for an insecure backend.
Your server should also consider:
HTTPS
TLS configuration
HSTS
Secure cookies
HttpOnly cookies
SameSite policy
Content Security Policy
Input validation
Output encoding
Authentication
Authorization
Think of the complete security boundary as:
Android App
+
WebView Configuration
+
Navigation Policy
+
Web Application
+
Backend
16. Recommended Project Structure
For a Clean Architecture Android project:
webview/
│
├── presentation/
│ ├── SecureWebView.kt
│ ├── WebViewScreen.kt
│ └── WebViewUiState.kt
│
├── security/
│ ├── UrlValidator.kt
│ ├── SecureWebViewClient.kt
│ └── WebViewSecurityConfig.kt
│
└── domain/
└── WebViewNavigationPolicy.kt
This keeps security rules reusable and testable.
17. Test the Security Boundary
Do not test only valid URLs.
Test cases should include:
https://example.com
https://www.example.com/article
http://example.com
https://example.com.attacker.com
https://attacker.com
javascript:alert(1)
file:///etc/hosts
content://some-provider/resource
A typical policy might be:
| URL Type | Expected Result |
|---|---|
| Trusted HTTPS | Allow |
| HTTP | Block |
| Unknown domain | Block |
| Lookalike domain | Block |
javascript: |
Block |
file: |
Block |
content: |
Block |
Always define expected behavior according to your application's actual security requirements.
18. Common WebView Security Mistakes
Mistake 1 — Loading arbitrary URLs
webView.loadUrl(userProvidedUrl)
Better: validate before loading.
Mistake 2 — Ignoring SSL errors
handler?.proceed()
Better: cancel unexpected certificate failures.
Mistake 3 — Enabling JavaScript unnecessarily
javaScriptEnabled = true
Better: enable it only when required.
Mistake 4 — Exposing a powerful JavaScript bridge
addJavascriptInterface(
PowerfulAndroidApi(),
"Android"
)
Better: expose the smallest possible API surface.
Mistake 5 — Using URL prefix matching
url.startsWith("https://example.com")
Better: parse the URL and validate the actual host.
19. Production Security Checklist
Network
- [ ] HTTPS is mandatory where appropriate.
- [ ] Certificate errors are not ignored.
- [ ] Cleartext traffic is disabled where appropriate.
- [ ] Backend TLS configuration is valid.
Navigation
- [ ] Trusted domains are allowlisted.
- [ ] Redirects are considered.
- [ ] External navigation has a defined policy.
- [ ] Deep-link URLs are validated.
JavaScript
- [ ] JavaScript is disabled when unnecessary.
- [ ] JavaScript bridges are minimized.
- [ ] Sensitive Android APIs are not exposed.
Local Access
- [ ] File access is disabled when unnecessary.
- [ ] Content access is disabled when unnecessary.
- [ ] Local resources are not unintentionally exposed.
Data
- [ ] Sensitive information is not placed in URLs.
- [ ] Authentication cookies are configured securely.
- [ ] Tokens are not exposed to arbitrary JavaScript.
Logging
- [ ] Navigation failures are observable.
- [ ] SSL failures are observable.
- [ ] Sensitive information is excluded from logs.
20. A Compact Secure Configuration
A minimal production-oriented configuration can look like:
WebView(context).apply {
settings.apply {
javaScriptEnabled = false
allowFileAccess = false
allowContentAccess = false
setSupportMultipleWindows(false)
safeBrowsingEnabled = true
}
webViewClient = SecureWebViewClient(
allowedHosts = setOf(
"example.com",
"www.example.com"
)
)
}
The goal is not to copy a fixed configuration into every application.
The goal is to understand which WebView capabilities your application actually requires and disable everything else.
Conclusion
A WebView should be treated as a security-sensitive component, not just another UI widget.
A strong implementation combines:
Trusted URL validation
+
HTTPS
+
Restricted WebView settings
+
Safe Browsing
+
Controlled JavaScript
+
Minimal JavaScript bridges
+
Safe SSL handling
+
Controlled external navigation
+
Secure backend configuration
The most important lesson is simple:
Build the security policy first, then enable only the WebView capabilities your application needs.
This makes the wrapper easier to reuse, test, audit, and maintain.
Security Review Checklist
Before releasing a WebView-based Android application, ask:
Can an attacker control the URL?
Can an untrusted domain load inside WebView?
Can JavaScript access native APIs?
Can WebView access local files?
Can an SSL error be bypassed?
Can an external intent inject a URL?
Can sensitive data reach JavaScript?
Are authentication cookies configured securely?
If the answer to any of these questions is unclear, the WebView security boundary deserves another review.
Support My Work
If this article helped you, you can support my open-source and technical writing work:
☕ Buy Me a Coffee:
https://buymeacoffee.com/padmakargarg
About the Author
Padmakar Garg is an Android Developer focused on Kotlin, Jetpack Compose, Android architecture, application security, and modern mobile development.
If you found this guide useful, feel free to share it with other Android developers working with WebView and mobile security.
Further Reading
- Android WebView documentation
- Android Network Security Configuration
- OWASP Mobile Application Security
- OWASP Cross-Site Scripting Prevention
- Content Security Policy documentation
Top comments (0)