10、WebView证书绑定:WebView安全配置、WebView证书校验、WebView证书绑定实现
聊到Android网络攻防,WebView绝对是个绕不开的坎儿。我见过太多应用因为WebView配置不当,被中间人攻击搞得体无完肤。说白了,WebView就是个浏览器内核的壳,你如果不对它做证书校验,那跟裸奔没啥区别。
今天咱们就深入聊聊WebView的证书绑定。我个人习惯把这块分成三个层次:安全配置、证书校验、证书绑定实现。一层比一层严格,一层比一层安全。
WebView安全配置:基础防线
先说说最基础的安全配置。很多开发者觉得WebView默认就是安全的,其实不然。我遇到过不少项目,WebView直接允许了混合内容,结果HTTPS页面里加载了HTTP的图片,整个加密通道就被污染了。
核心配置要点:
- 禁止混合内容:
setMixedContentMode(MIXED_CONTENT_NEVER_ALLOW) - 启用JavaScript时注意XSS风险
- 关闭文件访问:
getSettings().setAllowFileAccess(false) - 禁用地理定位等敏感API
// 基础安全配置示例
WebView webView = findViewById(R.id.webview);
WebSettings settings = webView.getSettings();
// 禁止混合内容
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webView.getSettings().setMixedContentMode(
WebSettings.MIXED_CONTENT_NEVER_ALLOW
);
}
// 关闭文件访问
settings.setAllowFileAccess(false);
settings.setAllowFileAccessFromFileURLs(false);
settings.setAllowUniversalAccessFromFileURLs(false);
// 谨慎启用JavaScript
settings.setJavaScriptEnabled(true);
// 但关闭JavaScript接口的自动绑定
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");
我的经验:曾经有个项目,测试环境一切正常,上线后用户反馈页面加载异常。查了半天,发现是测试服务器用了自签名证书,而生产环境是正规CA证书。WebView默认对自签名证书会报错,但很多人直接重写了onReceivedSslError并调用了proceed()——这是大忌!
WebView证书校验:拦截中间人
基础配置只是第一步。真正的安全防线在于证书校验。WebView默认会校验服务器证书的合法性,但有个问题:它信任系统根证书。如果攻击者在用户手机上安装了恶意CA证书,那WebView就会信任伪造的服务器。
为什么会这样?因为Android的证书信任链是基于系统根证书的。一旦根证书被污染,整个信任体系就崩塌了。所以我们需要自己实现证书校验逻辑。
// 自定义WebViewClient进行证书校验
webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// 不要直接调用handler.proceed()
// 应该根据错误类型做判断
switch (error.getPrimaryError()) {
case SslError.SSL_UNTRUSTED:
// 证书不受信任
handler.cancel();
break;
case SslError.SSL_EXPIRED:
// 证书过期
handler.cancel();
break;
case SslError.SSL_IDMISMATCH:
// 域名不匹配
handler.cancel();
break;
case SslError.SSL_NOTYETVALID:
// 证书尚未生效
handler.cancel();
break;
default:
handler.cancel();
break;
}
}
});
注意:千万不要在onReceivedSslError中直接调用handler.proceed()。我曾经审计过一个金融类应用,他们为了测试方便,直接忽略了所有SSL错误。结果上线后,攻击者用伪造证书就能劫持所有WebView流量。这个漏洞被评为高危。
WebView证书绑定实现:终极方案
证书绑定(Certificate Pinning)才是真正的终极方案。它的原理很简单:在客户端硬编码服务器证书的公钥或证书指纹,只信任这些特定的证书。就算系统根证书被污染,攻击者也无法用其他证书冒充你的服务器。
我建议使用公钥指纹进行绑定,而不是整个证书。因为证书会过期轮换,但公钥可以保持不变。这样你更新证书时,客户端代码不需要跟着改。
// 证书绑定实现
public class PinningWebViewClient extends WebViewClient {
// 预置的合法证书公钥指纹(SHA-256)
private static final Set<String> VALID_PINS = new HashSet<>(Arrays.asList(
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
));
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// 获取服务器证书链
X509Certificate[] certChain = getCertificateChain(error);
if (certChain != null && verifyPins(certChain)) {
// 证书指纹匹配,放行
handler.proceed();
} else {
// 证书不匹配,拦截
handler.cancel();
// 可以在这里记录日志或上报异常
Log.w("PinningWebView", "Certificate pinning failed!");
}
}
private boolean verifyPins(X509Certificate[] chain) {
try {
for (X509Certificate cert : chain) {
// 计算公钥的SHA-256指纹
byte[] publicKey = cert.getPublicKey().getEncoded();
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(publicKey);
String pin = "sha256/" + Base64.encodeToString(digest, Base64.NO_WRAP);
// 检查是否在预置列表中
if (VALID_PINS.contains(pin)) {
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private X509Certificate[] getCertificateChain(SslError error) {
// 从SslError中提取证书链
// 具体实现依赖于Android版本,这里简化处理
return null;
}
}
关键点总结:
- 绑定公钥指纹而非整个证书,便于证书轮换
- 至少绑定两个备用公钥,一个当前用,一个备用
- 不要只绑定叶子证书,也要考虑中间CA证书
- 定期更新绑定的指纹列表
知识体系结构图
避坑指南:我曾经在一个项目中,只绑定了叶子证书的公钥。结果证书过期后,新证书的公钥变了,所有用户都无法访问。后来我改成绑定两个公钥——一个当前用,一个备用。这样证书轮换时,客户端不需要更新。记住,永远给自己留条后路。
嗯,WebView证书绑定这块,说白了就是让应用只信任你指定的证书。三层防护层层递进:安全配置打基础,证书校验做拦截,证书绑定上保险。我个人建议,如果应用涉及敏感数据传输,直接上证书绑定,别犹豫。
最后提醒一句:证书绑定虽然安全,但也要考虑运维成本。证书轮换、紧急回滚、多环境支持,这些都要提前设计好。别等到线上出问题了才手忙脚乱。