17、输入验证与注入防护:SQL注入防护、命令注入防护、XSS防护、Intent注入防护
说到注入攻击,我脑子里立刻浮现出几年前的一个深夜。
那天凌晨三点,线上告警响个不停——用户数据被批量拖走。查了半天,问题出在一个搜索框上。开发同学图省事,直接把用户输入拼进了SQL语句。嗯,一个引号都没过滤。
从那以后,我对输入验证这件事,再也不敢马虎。
注入攻击的本质是什么?
说白了就一句话:用户输入被当成了代码执行。
你想想看,你的应用接收了一段文本,本意是当数据用。结果攻击者塞了一段特殊字符,你的程序没识别出来,直接把它当成命令去跑了。这就是注入。
Android里常见的注入有四种:SQL注入、命令注入、XSS、Intent注入。咱们一个一个说。
核心原则:永远不要信任用户的输入。任何来自外部的东西——文本框、文件、Intent参数、ContentProvider查询——都要当成潜在的攻击载荷。
SQL注入防护
Android本地数据库多用SQLite。我见过不少项目,直接拿字符串拼接写SQL:
// 危险写法!千万别学
String query = "SELECT * FROM users WHERE name = '" + userName + "'";
Cursor cursor = db.rawQuery(query, null);
如果userName传进来的是 ' OR 1=1 --,那整张表就全暴露了。
正确的做法:用参数化查询。
// 安全写法
String query = "SELECT * FROM users WHERE name = ?";
Cursor cursor = db.rawQuery(query, new String[]{userName});
我个人习惯,只要涉及数据库操作,一律用 ? 占位符。哪怕只是查个ID,也不偷懒。
小技巧:Room框架已经帮你封装好了参数绑定。如果你还在手写SQLiteHelper,建议尽快迁移到Room。少写很多坑。
命令注入防护
这个场景在Android里相对少见,但一旦出事就是大事。
我记得有个项目需要调用系统命令获取设备信息,开发同学直接写了:
Runtime.getRuntime().exec("ping " + ipAddress);
如果ipAddress传进来的是 8.8.8.8; rm -rf /……嗯,后果你自己想。
防护方案:
- 能用Java API就别调系统命令
- 实在要调,用白名单校验输入
- 用
ProcessBuilder代替Runtime.exec,把参数拆开传
// 相对安全的写法
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", sanitizedIp);
Process process = pb.start();
这里的关键是:ProcessBuilder 会把每个参数当成独立字符串,不会把分号、管道符解析成命令分隔符。
注意:就算用了ProcessBuilder,输入仍然要校验。我曾经见过有人传了 127.0.0.1; echo hacked,虽然没执行成功,但日志里留下了脏数据。能拦就拦在前面。
XSS防护
XSS在Android里主要出现在WebView场景。你加载了一个网页,网页里嵌了恶意脚本,脚本就能通过JavaScript接口调用你的Native方法。
我见过最典型的案例:某个金融App的WebView开了JavaScript支持,还加了 @JavascriptInterface 注解的方法。结果攻击者在论坛发帖里嵌了一段脚本,脚本调用了那个接口,直接读取了用户的通讯录。
防护要点:
- 非必要不开启JavaScript
- 必须开启时,对渲染的内容做HTML转义
- 用
WebView.getSettings().setAllowFileAccess(false)关掉文件访问 - 对
@JavascriptInterface暴露的方法做权限校验
// HTML转义示例
public static String escapeHtml(String input) {
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
避坑指南:我曾经遇到一个项目,开发同学觉得"用户输入的只是昵称,不会有问题",结果昵称里带了 <script> 标签。嗯,那个版本上线三天就紧急回滚了。从那以后,我所有展示用户输入的地方,一律做转义。
Intent注入防护
这个坑比较隐蔽。Android的Intent可以携带数据,也可以启动组件。如果攻击者构造了一个恶意Intent发给你的App,你的App又没做校验,就可能被利用。
典型场景:你的App有一个 exported=true 的Activity,接收外部Intent。攻击者传了一个带恶意Extra的Intent进来,你的代码直接取出来用——比如取了一个URL去加载。
防护方案:
- 非必要不设置
exported=true - 必须暴露的组件,对Intent中的数据做校验
- 用
Intent.getData()获取URI时,检查scheme和host - 对Extra中的字符串做白名单校验
// Intent数据校验示例
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri data = intent.getData();
if (data != null && "https".equals(data.getScheme())
&& "mysafe.com".equals(data.getHost())) {
// 只有白名单内的URL才处理
loadUrl(data.toString());
} else {
// 拒绝处理
finish();
}
}
特别提醒:别用黑名单。攻击者总有办法绕过。白名单才是正道。
知识体系总览
下面这张图总结了四种注入攻击的入口、风险点和防护手段。你可以把它当成一个快速检查清单。
写在最后
注入攻击的防护,说到底就两件事:验证输入和转义输出。
验证输入,确保进来的数据符合预期格式。转义输出,确保数据不会变成代码被执行。
这两件事做扎实了,90%的注入漏洞就跟你没关系了。
剩下的10%,靠代码审查和自动化工具来补。我个人习惯在CI流程里加一个静态扫描步骤,专门检查SQL拼接和 Runtime.exec 调用。发现一个,打回一个。
安全开发不是一朝一夕的事。但只要你养成了"先验证,再使用"的习惯,很多坑自然就绕过去了。
一句话总结:用户输入是洪水,你的代码是堤坝。堤坝修得牢不牢,决定了你的App会不会被冲垮。