移动 WebView 桥接、协议与证书安全加固
防御目标与威胁面
移动应用 WebView 常见的系统性失陷链包括:
- JS Bridge 过度暴露:通过
addJavascriptInterface向不可信页面暴露文件读取或返回Context/SystemService的方法(参考 CVE-2022-36916 链),或 iOSWKScriptMessageHandler未校验message.frameInfo.securityOrigin。 - Deeplink 参数直通
loadUrl:外部intent:///myapp://open?url=...未做协议与域名白名单校验即传入webView.loadUrl(url),引发javascript:XSS、file://读私有目录或钓鱼跳转。 - 文件协议与本地访问开启:
setAllowFileAccess(true)或setAllowFileAccessFromFileURLs(true)允许file://页面跨域读取/data/data/<pkg>/shared_prefs/与databases/。 - SSL 校验静默放行:
WebViewClient.onReceivedSslError调用handler.proceed()(哪怕仅包裹在误判的调试条件中)导致中间人劫持与 JS 注入。 - Cookie 与进程池污染:多个域或业务共享敏感会话 Cookie 或 iOS
WKProcessPool未隔离。
核心防御矩阵
| 攻击向量 | 根治控制措施 | 关键实现约束 |
|---|---|---|
addJavascriptInterface 反射链 / 任意域调用 | 改用 WebViewCompat.addWebMessageListener 并严格限定 allowedOriginRules | 禁止暴露返回 Context/文件流/通用命令的方法;minSdkVersion >= 17 且绝不配置 "*" 源 |
iOS WKScriptMessageHandler 跨域 iframe 触发 | 在 userContentController 校验 message.frameInfo.isMainFrame 与 securityOrigin.host | 仅放行精确 HTTPS 白名单主源,不共享跨信任域 WKProcessPool |
Deeplink 注入 javascript: / file: / 外部域 | 统一使用 Uri 解析强制 scheme == "https" 且 host 命中精确白名单 | 在 loadUrl 前和 shouldOverrideUrlLoading 双重校验,拒绝 intent:/data:/content: |
file:// 与 content:// 私有文件读取 | 显式关闭 allowFileAccess、allowContentAccess 及 allowFileAccessFromFileURLs | 本地静态资源统一迁移至 WebViewAssetLoader 通过 https://appassets.androidplatform.net/ 加载 |
onReceivedSslError 放行中间人证书 | 强制重写 onReceivedSslError 仅调用 handler.cancel() | 禁止保留 handler.proceed(),并配合 network_security_config.xml 约束信任锚点 |
加固代码与静态核查示例
1. Android Kotlin 加固 WebView 初始化与严格 Origin 校验
kotlin
// HardenedWebViewFactory.kt — 关闭文件访问、强制 HTTPS 白名单与 Origin 绑定通信
package com.example.security
import android.net.Uri
import android.net.http.SslError
import android.webkit.SslErrorHandler
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
object HardenedWebViewFactory {
private val ALLOWED_HOSTS = setOf("app.example.com", "appassets.androidplatform.net")
fun isTrustedHttpsUri(rawUrl: String?): Boolean {
if (rawUrl.isNullOrBlank()) return false
val uri = runCatching { Uri.parse(rawUrl) }.getOrNull() ?: return false
return uri.scheme == "https" && uri.host in ALLOWED_HOSTS
}
fun configure(webView: WebView) {
val settings = webView.settings
settings.javaScriptEnabled = true
settings.allowFileAccess = false
settings.allowContentAccess = false
@Suppress("DEPRECATION")
settings.allowFileAccessFromFileURLs = false
@Suppress("DEPRECATION")
settings.allowUniversalAccessFromFileURLs = false
settings.setSupportMultipleWindows(false)
val assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(webView.context))
.build()
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
// 非白名单域名一律阻断,不交由 WebView 加载
return !isTrustedHttpsUri(request.url.toString())
}
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(request.url)
}
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
handler.cancel()
}
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(
webView,
"secureBridge",
setOf("https://app.example.com")
) { _, message, sourceOrigin, isMainFrame, _ ->
if (!isMainFrame || sourceOrigin.toString() != "https://app.example.com") {
return@addWebMessageListener
}
// 仅处理严格 JSON schema 指令
handleValidatedBridgeMessage(message.data)
}
}
}
private fun handleValidatedBridgeMessage(data: String?) {
if (data.isNullOrEmpty()) return
// 按最小权限业务白名单分发
}
}2. Python 静态扫描 Android WebView 危险配置反模式
python
import re
from typing import Dict, List
WEBVIEW_ANTIPATTERNS: Dict[str, re.Pattern[str]] = {
"ssl_proceed": re.compile(r"onReceivedSslError[\s\S]{0,200}?handler\.proceed\s*\("),
"file_access_enabled": re.compile(r"setAllowFileAccess\s*\(\s*true\s*\)"),
"universal_file_access": re.compile(r"setAllow(?:File|Universal)AccessFromFileURLs\s*\(\s*true\s*\)"),
"legacy_js_interface": re.compile(r"addJavascriptInterface\s*\("),
"raw_intent_loadurl": re.compile(r"getQueryParameter\([^)]+\)[\s\S]{0,120}?loadUrl\s*\("),
}
def scan_webview_source(source_code: str) -> List[str]:
return [name for name, pat in WEBVIEW_ANTIPATTERNS.items() if pat.search(source_code)]
if __name__ == "__main__":
vuln_snippet = """
webView.getSettings().setAllowFileAccess(true);
webView.addJavascriptInterface(new FileAccessor(), "bridge");
String url = getIntent().getData().getQueryParameter("url");
webView.loadUrl(url);
public void onReceivedSslError(WebView v, SslErrorHandler handler, SslError e) {
handler.proceed();
}
"""
hits = scan_webview_source(vuln_snippet)
assert set(hits) == {"ssl_proceed", "file_access_enabled", "legacy_js_interface", "raw_intent_loadurl"}
safe_snippet = """
settings.allowFileAccess = false
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
handler.cancel()
}
"""
assert scan_webview_source(safe_snippet) == []
print("WebView defense pattern check passed:", hits)防御工作流
text
1. 清理文件协议:全量检索关闭 setAllowFileAccess/setAllowContentAccess,改用 WebViewAssetLoader 托管本地资源
2. 升级桥接机制:废弃任意域可调用的 addJavascriptInterface,迁移至限制 origin 与主帧的 WebMessageListener
3. 严控外部入口:对所有 Intent/Deeplink 参数先解析 Uri,强制仅接受白名单 HTTPS 域名,屏蔽 javascript:/file:/intent:
4. 消除 SSL 绕过:全库清理 handler.proceed(),统一执行 handler.cancel() 并搭配系统 Network Security Config
5. 隔离会话凭证:Cookie 启用 HttpOnly+Secure+SameSite,不跨业务线共享敏感态 WebView 容器或 WKProcessPool验证与判定标准
- Deeplink 协议拦截测试:使用
adb shell am start传入?url=javascript:alert(1)、?url=file:///data/data/...及外部恶意域名时,WebView 必须拒绝加载且不得触发任何 JS 执行。 - 桥接域隔离测试:在测试页面嵌入第三方域
iframe尝试调用secureBridge时,消息监听器因isMainFrame == false或sourceOrigin不匹配必须静默丢弃。 - 证书错误阻断测试:将代理自签名证书指向 WebView 请求时,必须触发
handler.cancel()中止连接且不渲染代理注入内容。
MCP 工具映射
| 防御步骤 | MCP 工具 | 说明 |
|---|---|---|
| 检查应用 Manifest 与导出组件 | android_app_baseline | 识别接收 Deeplink 的导出 Activity 与应用包配置 |
| 验证 HTTPS 与证书错误行为 | android_http_observation_recipe | 核对代理场景下 WebView 是否严格拒绝异常证书 |
| 检索对应红蓝文档 | kb_router | 对照 01-webview-attacks.md 覆盖全部桥接与协议攻击面 |