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>,转义后就变成了 &lt;script&gt;alert('xss')&lt;/script&gt;,浏览器只会把它当作普通文本显示,不会执行。

个人建议:如果WebView只是展示静态内容,直接禁用JavaScript。如果必须启用,考虑使用 WebViewClientshouldOverrideUrlLoading 做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,指定目标组件的包名和类名,问题就解决了。

知识体系总览

下面这张图总结了四种注入攻击的入口、危害和防护手段。我建议你把它存下来,写代码前瞄一眼。

注入攻击防护知识体系 输入验证与注入防护 SQL注入 入口:输入框、搜索接口 危害:数据泄露、篡改 防护:参数化查询 命令注入 入口:Runtime.exec() 危害:执行恶意命令 防护:ProcessBuilder XSS攻击 入口:WebView、富文本 危害:脚本执行、数据窃取 防护:HTML转义 Intent注入 入口:外部Intent 危害:越权、启动恶意组件 防护:白名单校验 核心原则 1. 永远不信任外部输入 2. 区分数据与代码(参数化、转义、白名单) 3. 最小权限原则:能不用命令就不用,能禁用JS就禁用 4. 多层防御:输入校验 + 输出编码 + 运行时检测

好了,四种注入攻击的防护方法都讲完了。其实核心就一句话:把输入当敌人,把参数化当武器。写代码的时候多问自己一句:这个输入会不会被当作代码执行?如果是,那就得防。

最后提醒:注入攻击的防护不是加一行代码就完事的。它需要你在设计阶段就考虑好数据流向,在编码阶段严格执行安全规范,在测试阶段用工具扫描一遍。我个人的习惯是,每个接口都做输入校验,每个数据库操作都用参数化,每个WebView都检查JS开关。虽然麻烦点,但总比出事了再补救强。


公众号:蓝海资料掘金营,微信deep3321