输入验证与注入防护:SQL注入、命令注入、XSS与Intent注入

说实话,在Android安全领域摸爬滚打这么多年,注入漏洞一直是我最头疼的问题之一。为什么?因为它太容易出现了。你想想看,只要应用接收了外部输入,没有做好验证,就可能在不知不觉中把控制权交了出去。

我个人习惯把注入漏洞比作「后门钥匙」。攻击者通过精心构造的输入,绕过你的安全防线,直接操作底层系统。今天我们就来聊聊四种最常见的注入攻击,以及如何从代码层面彻底封死这些后门。

核心观点:一切输入都是不可信的。这是安全开发的铁律,没有例外。

输入验证与注入防护 SQL注入 命令注入 XSS攻击 Intent注入 防护:参数化查询 ContentProvider权限 输入过滤 防护:禁用Runtime.exec 白名单命令 参数转义 防护:WebView配置 HTML编码 CSP策略 防护:显式Intent 组件权限检查 Intent过滤 核心原则:永远不要信任外部输入

一、SQL注入:数据库的隐形杀手

SQL注入在Web领域是老生常谈了,但在Android上同样致命。我记得有一次做安全审计,发现某个社交应用的本地数据库查询居然直接拼接用户输入。攻击者只要在搜索框里输入 ' OR 1=1 --,就能把整个用户表拖出来。

为什么会这样?说白了,就是代码把用户输入当成了SQL语句的一部分,而不是单纯的数据。

危险代码示例:

// 千万不要这样写!
String query = "SELECT * FROM users WHERE name = '" + userName + "'";
Cursor cursor = db.rawQuery(query, null);

正确的做法是什么?参数化查询。这是最根本的解决方案。

// 安全写法:使用参数化查询
String query = "SELECT * FROM users WHERE name = ?";
Cursor cursor = db.rawQuery(query, new String[]{userName});

如果你用的是Room,那就更省心了。Room在编译时就会生成参数化查询的代码,你只要用 @Query 注解写SQL就行。

我的经验:在ContentProvider中也要注意。如果Provider暴露给外部应用,一定要检查传入的URI参数。我曾经见过一个Provider直接把URI参数拼接到SQL里,结果第三方应用可以随意查询数据库。

二、命令注入:别让用户控制你的终端

命令注入在Android上相对少见,但一旦出现就是灾难。你想想看,如果攻击者能通过你的应用执行 rm -rf / 或者 pm uninstall,那后果不堪设想。

我遇到过最典型的场景:某个文件管理器应用,用户选择文件后调用 Runtime.getRuntime().exec("chmod 777 " + filePath)。攻击者只要把文件名改成 test; rm -rf /sdcard/,就能执行任意命令。

高危操作:

// 绝对不要这样做!
String cmd = "ls -l " + userInput;
Process process = Runtime.getRuntime().exec(cmd);

防护方案其实很简单:

  • 能不执行命令就别执行。大部分功能都有Java API替代方案
  • 必须执行时,用白名单。只允许预定义的命令和参数
  • 对参数做转义。去掉 ; & | $ ` 等特殊字符
// 安全做法:白名单+参数校验
private static final Set<String> ALLOWED_COMMANDS = 
    new HashSet<>(Arrays.asList("ls", "cat", "echo"));

public void safeExec(String command, String arg) {
    if (!ALLOWED_COMMANDS.contains(command)) {
        throw new SecurityException("命令不在白名单中");
    }
    // 对参数做严格校验
    if (arg.contains(";") || arg.contains("|")) {
        throw new SecurityException("参数包含非法字符");
    }
    Process process = Runtime.getRuntime().exec(
        new String[]{command, arg}
    );
}

避坑指南:我曾经用 ProcessBuilder 替代 Runtime.exec(),因为前者支持传入字符串数组,不会经过shell解析,天然防注入。但要注意,如果用了 ProcessBuilder("sh", "-c", cmd),那又回到shell环境了,等于白费功夫。

三、XSS攻击:WebView里的暗箭

XSS(跨站脚本攻击)在Android上主要出现在WebView场景。你的应用加载了网页内容,如果这些内容包含恶意脚本,攻击者就能通过JavaScript偷取用户数据、劫持会话。

我记得有个电商应用,商品详情页用WebView展示HTML描述。攻击者在商品描述里嵌入了 <script>stealCookie()</script>,结果所有浏览该商品的用户cookie都被窃取了。

防护措施分几个层面:

防护层面 具体措施 说明
WebView配置 禁用JavaScript 不需要JS交互时,直接关掉
内容编码 HTML实体编码 < 转为 &lt;
CSP策略 Content Security Policy 限制脚本来源和执行
输入过滤 移除script标签 使用白名单HTML解析器
// WebView安全配置示例
WebView webView = findViewById(R.id.webview);
WebSettings settings = webView.getSettings();

// 不需要JS就关掉
settings.setJavaScriptEnabled(false);

// 如果必须用JS,至少禁用文件访问
settings.setAllowFileAccess(false);
settings.setAllowContentAccess(false);

// 加载内容时做HTML编码
String safeHtml = TextUtils.htmlEncode(userInput);
webView.loadDataWithBaseURL(null, safeHtml, 
    "text/html", "UTF-8", null);

特别注意:如果使用了 addJavascriptInterface,一定要在API 17以上用 @JavascriptInterface 注解标记暴露的方法。低于API 17的设备,所有公有方法都会暴露给JS,这是巨大的安全漏洞。

四、Intent注入:组件间的陷阱

Intent注入是Android特有的问题。当你的应用通过Intent接收外部数据,或者发送隐式Intent时,攻击者可能构造恶意的Intent数据来操纵你的应用行为。

我印象最深的一个案例:某个支付应用通过隐式Intent启动支付页面,攻击者构造了一个恶意Intent,把支付金额改成了0.01元,然后自己发起支付请求。虽然金额在后端有校验,但前端显示已经被篡改了。

防护方案其实不复杂:

  • 使用显式Intent。明确指定目标组件,不要用action匹配
  • 检查Intent来源。通过 getCallingPackage() 验证调用者
  • 对Intent中的extra数据做校验。类型检查、范围检查
// 安全接收Intent
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    Intent intent = getIntent();
    
    // 检查调用者身份
    String callingPackage = getCallingPackage();
    if (!"com.trusted.app".equals(callingPackage)) {
        // 记录异常,拒绝处理
        return;
    }
    
    // 对extra数据做严格校验
    String action = intent.getStringExtra("action");
    if (!isValidAction(action)) {
        throw new IllegalArgumentException("非法操作");
    }
    
    // 使用显式Intent启动组件
    Intent safeIntent = new Intent(this, TargetActivity.class);
    safeIntent.putExtra("data", sanitizedData);
    startActivity(safeIntent);
}

我的习惯:对于暴露给外部应用的组件,我通常会在AndroidManifest中设置 android:exported="false",除非确实需要外部调用。如果必须暴露,就加上自定义权限,并在代码中做权限检查。

总结:输入验证的黄金法则

说了这么多,其实核心就一句话:永远不要信任外部输入。不管是用户输入的文本、来自其他应用的Intent,还是网络请求返回的数据,都要经过严格的验证和清洗。

我个人习惯在应用的入口层(Activity、Service、BroadcastReceiver、ContentProvider)统一做输入验证,而不是分散在各个业务逻辑里。这样既容易维护,也不容易遗漏。

嗯,最后提醒一点:安全防护不是一劳永逸的。攻击手法在进化,我们的防护也要持续更新。定期做代码审计、关注Android安全公告,这些都是必修课。


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