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();
    }
}

特别提醒:别用黑名单。攻击者总有办法绕过。白名单才是正道。

知识体系总览

下面这张图总结了四种注入攻击的入口、风险点和防护手段。你可以把它当成一个快速检查清单。

Android注入攻击防护知识体系 用户输入 SQL注入 命令注入 XSS Intent注入 风险:数据泄露 风险:远程执行 风险:接口调用 风险:组件劫持 ✅ 参数化查询 ✅ 使用Room框架 ✅ 白名单校验 ✅ ProcessBuilder ✅ HTML转义 ✅ 关闭JS接口 ✅ 白名单校验 ✅ 关闭exported 核心原则:不信任任何输入 · 白名单优先 · 最小权限 所有外部数据在进入业务逻辑前,必须经过验证和转义

写在最后

注入攻击的防护,说到底就两件事:验证输入转义输出

验证输入,确保进来的数据符合预期格式。转义输出,确保数据不会变成代码被执行。

这两件事做扎实了,90%的注入漏洞就跟你没关系了。

剩下的10%,靠代码审查和自动化工具来补。我个人习惯在CI流程里加一个静态扫描步骤,专门检查SQL拼接和 Runtime.exec 调用。发现一个,打回一个。

安全开发不是一朝一夕的事。但只要你养成了"先验证,再使用"的习惯,很多坑自然就绕过去了。

一句话总结:用户输入是洪水,你的代码是堤坝。堤坝修得牢不牢,决定了你的App会不会被冲垮。

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