7、输入验证与注入防护:SQL注入防护、命令注入防护、XSS防护、Intent注入防护
说到注入攻击,我脑子里立刻浮现出几年前的一个深夜。
那天线上突然爆出一批用户数据异常,查了半天,发现是某个搜索接口没做输入过滤,被人塞了一段SQL进去。嗯,从那以后,我对输入验证这件事就特别敏感。
说白了,注入攻击的本质就一句话:你把用户输入当代码执行了。不管是SQL、系统命令、HTML脚本还是Intent,只要没做区分,攻击者就能钻空子。
核心原则:永远不要信任任何外部输入。用户输入、网络数据、文件内容、Intent参数——统统当作恶意数据来处理。
7.1 SQL注入防护
SQL注入是最经典的注入攻击。攻击者在输入框里塞一段SQL语句,如果你的代码直接拼接字符串去查数据库,那数据库就裸奔了。
举个例子,你可能会写出这样的代码:
// 危险!直接拼接SQL
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Cursor cursor = db.rawQuery(query, null);
如果用户输入 username = admin' --,那SQL就变成了:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'xxx'
后面的密码验证直接被注释掉了。攻击者连密码都不用知道就能登录。
正确的做法是什么?用参数化查询。Android里就是 selectionArgs:
// 安全!使用参数化查询
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
Cursor cursor = db.rawQuery(query, new String[]{username, password});
我个人习惯,只要涉及数据库操作,一律用参数化。哪怕只是查个ID,也绝不偷懒拼字符串。
小技巧:Room数据库默认就是参数化绑定的,所以用Room能天然避免SQL注入。我建议新项目直接上Room,别再用rawQuery了。
7.2 命令注入防护
命令注入在Android里不太常见,但一旦出现,后果很严重。攻击者通过输入特殊字符,让你执行了额外的系统命令。
我曾经在某个项目中看到这样的代码:
// 危险!直接拼接命令
String cmd = "ping -c 1 " + ipAddress;
Runtime.getRuntime().exec(cmd);
如果用户输入 8.8.8.8; rm -rf /,那你的手机可能就被清空了。
防护方法:
- 尽量避免执行系统命令。能用Java API解决的,就别调Runtime。
- 如果必须执行,用白名单验证输入。只允许特定的字符集。
- 使用
ProcessBuilder代替Runtime.exec(),因为它支持参数分离。
// 安全做法:参数分离
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", ipAddress);
Process process = pb.start();
你看,参数分开了,攻击者就算输入 8.8.8.8; rm -rf /,也只会被当作一个IP地址字符串传给ping命令,不会被执行。
注意:ProcessBuilder也不是万能的。如果ipAddress里包含空格或特殊字符,仍然可能出问题。最好的办法是:对输入做严格的格式校验。比如IP地址,用正则匹配一下 \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}。
7.3 XSS防护
XSS(跨站脚本攻击)在Android里主要出现在WebView场景。攻击者把恶意JavaScript代码塞进页面,然后你的WebView就中招了。
我记得有个项目,用户评论里可以显示富文本。结果有人贴了一段 <script>alert('xss')</script>,WebView直接弹窗了。还好只是测试,要是换成窃取Cookie的代码,那就麻烦了。
防护要点:
- WebView设置:禁用JavaScript,除非绝对必要。
- 如果必须用JavaScript,使用
addJavascriptInterface时格外小心。 - 对输出到WebView的内容做HTML转义。
// HTML转义示例
public static String escapeHtml(String input) {
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
你想想看,如果用户输入 <script>alert('xss')</script>,转义后就变成了 <script>alert('xss')</script>,浏览器只会把它当作普通文本显示,不会执行。
个人建议:如果WebView只是展示静态内容,直接禁用JavaScript。如果必须启用,考虑使用 WebViewClient 的 shouldOverrideUrlLoading 做URL白名单校验。
7.4 Intent注入防护
Intent注入是Android特有的攻击方式。攻击者通过构造恶意的Intent,让你的应用执行非预期的操作。
最常见的场景是:你的应用接收外部Intent,然后直接用它启动Activity或Service。
// 危险!直接使用外部Intent
Intent intent = getIntent();
startActivity(intent); // 攻击者可以构造任意Intent
攻击者可以构造一个Intent,指向你的内部私有Activity,或者指向系统设置页面,甚至指向恶意应用。
防护方法:
- 使用
Intent.parseUri()时,设置URI_INTENT_SCHEME标志。 - 对接收到的Intent做白名单校验,只允许特定的Action和Component。
- 使用
PendingIntent时,明确指定目标组件。
// 安全做法:校验Intent
Intent intent = getIntent();
String action = intent.getAction();
if ("com.example.MY_ACTION".equals(action)) {
// 只处理已知的Action
startActivity(intent);
} else {
// 拒绝未知的Intent
Log.w("Security", "Unknown intent action: " + action);
}
说白了,就是不要盲目信任外部传进来的Intent。你想想看,如果有人给你寄了个包裹,你会不检查就直接拆吗?Intent也一样。
我曾经踩过的坑:有个功能需要从通知栏跳转到某个Activity,我直接用 PendingIntent.getActivity() 传了个隐式Intent。结果第三方应用通过Intent嗅探拦截了通知,跳转到了恶意页面。后来改成显式Intent,指定目标组件的包名和类名,问题就解决了。
知识体系总览
下面这张图总结了四种注入攻击的入口、危害和防护手段。我建议你把它存下来,写代码前瞄一眼。
好了,四种注入攻击的防护方法都讲完了。其实核心就一句话:把输入当敌人,把参数化当武器。写代码的时候多问自己一句:这个输入会不会被当作代码执行?如果是,那就得防。
最后提醒:注入攻击的防护不是加一行代码就完事的。它需要你在设计阶段就考虑好数据流向,在编码阶段严格执行安全规范,在测试阶段用工具扫描一遍。我个人的习惯是,每个接口都做输入校验,每个数据库操作都用参数化,每个WebView都检查JS开关。虽然麻烦点,但总比出事了再补救强。
公众号:蓝海资料掘金营,微信deep3321