DEV Community

Cover image for How to Build a Secure Android WebView Wrapper with Kotlin
Padmakar Android
Padmakar Android

Posted on

How to Build a Secure Android WebView Wrapper with Kotlin

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

1. Define Trusted Domains

Start with an explicit allowlist.

private val allowedHosts = setOf(
    "example.com",
    "www.example.com"
)
Enter fullscreen mode Exit fullscreen mode

Never rely on simple string prefix matching:

url.startsWith("https://example.com")
Enter fullscreen mode Exit fullscreen mode

A URL such as:

https://example.com.attacker.com
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

The basic policy becomes:

HTTPS + trusted host
        ↓
      Allow

Everything else
        ↓
      Block / handle externally
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it only when necessary:

webView.addJavascriptInterface(
    SecureBridge(),
    "AndroidBridge"
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

8. Never Ignore SSL Errors

One of the most dangerous WebView patterns is:

override fun onReceivedSslError(
    view: WebView?,
    handler: SslErrorHandler?,
    error: SslError?
) {
    handler?.proceed()
}
Enter fullscreen mode Exit fullscreen mode

Do not blindly proceed after certificate errors.

A safer default is:

override fun onReceivedSslError(
    view: WebView?,
    handler: SslErrorHandler?,
    error: SslError?
) {
    handler?.cancel()
}
Enter fullscreen mode Exit fullscreen mode

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.
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

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()
    )
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Instead:

val url = intent.getStringExtra("url")

if (url != null && isTrustedUrl(url, allowedHosts)) {
    webView.loadUrl(url)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 SameSite is 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
Enter fullscreen mode Exit fullscreen mode

Think of the complete security boundary as:

Android App
     +
WebView Configuration
     +
Navigation Policy
     +
Web Application
     +
Backend
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Better: validate before loading.


Mistake 2 — Ignoring SSL errors

handler?.proceed()
Enter fullscreen mode Exit fullscreen mode

Better: cancel unexpected certificate failures.


Mistake 3 — Enabling JavaScript unnecessarily

javaScriptEnabled = true
Enter fullscreen mode Exit fullscreen mode

Better: enable it only when required.


Mistake 4 — Exposing a powerful JavaScript bridge

addJavascriptInterface(
    PowerfulAndroidApi(),
    "Android"
)
Enter fullscreen mode Exit fullscreen mode

Better: expose the smallest possible API surface.


Mistake 5 — Using URL prefix matching

url.startsWith("https://example.com")
Enter fullscreen mode Exit fullscreen mode

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"
        )
    )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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)