14、Content Provider安全:SQL注入、目录遍历、权限泄露

Content Provider,说白了就是Android应用之间共享数据的“管道”。

我刚开始做安全测试那会儿,总觉得这玩意儿挺安全——毕竟有权限控制嘛。直到有一次,我随手测了一个记账App,发现它的Provider居然允许任意应用读取所有账单记录。嗯,从那以后,我再也不敢小看Content Provider了。

14.1 Content Provider的核心风险

Content Provider暴露了数据接口,就相当于给外界开了一扇门。门开多大,谁可以进,进去能拿什么——这三件事只要有一个没管好,漏洞就来了。

我个人习惯把Content Provider的安全问题归为三类:

  • SQL注入:拼接查询语句时,攻击者传入恶意SQL片段
  • 目录遍历:通过构造特殊路径,读取本不该访问的文件
  • 权限泄露:权限配置不当,导致未授权应用也能访问数据

核心观点:Content Provider的安全,本质上就是“谁、能做什么、在什么条件下做”这三个维度的控制。任何一个维度失控,数据就危险了。

14.2 SQL注入:最经典的Provider漏洞

先看一个典型的漏洞代码:

public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    // 危险!直接拼接用户输入
    String sql = "SELECT * FROM users WHERE name = '" + selection + "'";
    return db.rawQuery(sql, null);
}

你想想看,如果攻击者传入 ' OR 1=1 --,会发生什么?整张表的数据全被拉走了。

我在项目中遇到过更隐蔽的情况——有些开发者觉得用了selectionArgs就安全了,结果在projection参数里直接拼接列名。说白了,只要有任何用户输入被拼进了SQL语句,就有注入风险。

避坑指南:我曾经接手过一个项目,开发者在query()里用了参数化查询,但在delete()方法里直接拼了字符串。测试时只测了查询,没测删除,结果上线后被删了用户表。所以,所有CRUD操作都要检查,一个都不能漏。

正确的做法是:

// 安全写法:使用参数化查询
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    // selection 中只包含 ? 占位符
    // selectionArgs 提供实际参数值
    return db.query("users", projection, selection, selectionArgs, null, null, sortOrder);
}

14.3 目录遍历:文件型Provider的噩梦

当Content Provider用于提供文件访问时(比如openFile()方法),目录遍历就成了一大隐患。

来看一个我实际审计过的代码:

public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    String path = uri.getLastPathSegment();
    // 危险!没有过滤路径中的 ".."
    File file = new File(getContext().getFilesDir(), path);
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}

攻击者传入 content://com.example.provider/files/../../data/data/com.example/databases/secret.db,就能直接读到数据库文件。

警告:目录遍历漏洞的危害极大。攻击者不仅能读取应用私有数据,还能读取系统文件(比如/etc/hosts/proc/self/cmdline等)。我见过一个案例,攻击者通过目录遍历读到了应用的签名证书,直接伪造了更新包。

修复方案其实不复杂:

  • 对路径做规范化处理,移除 ...
  • 限制文件只能从特定目录读取
  • 使用 UriMatcher 做精确匹配,而不是直接取路径片段
// 安全写法:规范化路径 + 白名单检查
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    String path = uri.getLastPathSegment();
    // 规范化路径,移除 .. 和 .
    File file = new File(getContext().getFilesDir(), path).getCanonicalFile();
    // 检查文件是否在允许的目录内
    String allowedDir = getContext().getFilesDir().getCanonicalPath();
    if (!file.getPath().startsWith(allowedDir)) {
        throw new SecurityException("Access denied");
    }
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}

14.4 权限泄露:谁都能进的门

权限泄露,说白了就是门没锁好。常见的情况有:

  • exported="true" 但没配权限:任何应用都能访问
  • 权限级别太低:用了 normal 级别权限,用户安装时直接授权
  • 权限声明了但没强制检查:代码里忘了调用 checkCallingPermission()

我记得有一次测试一个社交App,它的Provider声明了 android:permission="com.example.READ_PROFILE",但这是个 normal 级别权限。结果呢?任何恶意应用只要在Manifest里声明一下这个权限,就能读到所有用户资料。这跟没锁门有什么区别?

个人经验:我建议把Provider的权限至少设为 dangerous 级别,并且在代码里做二次校验。别只依赖Manifest声明——万一有人通过反射或者adb直接调用呢?

正确的权限配置示例:

<!-- AndroidManifest.xml -->
<provider
    android:name=".MyProvider"
    android:authorities="com.example.provider"
    android:exported="true"
    android:readPermission="com.example.permission.READ_DATA"
    android:writePermission="com.example.permission.WRITE_DATA"
    android:protectionLevel="dangerous" />
// 代码中二次校验
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    // 检查调用者是否有权限
    int pid = Binder.getCallingPid();
    int uid = Binder.getCallingUid();
    if (checkCallingPermission("com.example.permission.READ_DATA") != 
        PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException("Permission denied");
    }
    // ... 正常查询逻辑
}

14.5 知识体系总览

下面这张图,是我梳理的Content Provider安全知识体系。你可以把它当成一个检查清单,测试时对着看,基本不会漏。

Content Provider 安全知识体系 Content Provider 安全 SQL注入 拼接查询字符串 projection参数未过滤 所有CRUD操作都要检查 目录遍历 openFile()路径未校验 使用getCanonicalFile() 白名单目录限制 权限泄露 exported=true无权限 权限级别太低 代码中二次校验 核心原则:谁 + 能做什么 + 在什么条件下做

14.6 自动化检测思路

手动测Provider确实累,我一般会写点自动化脚本辅助。核心思路就三条:

  1. 枚举所有暴露的Provider:用 adb shell dumpsys package 或者写个App遍历 PackageManager.queryContentProviders()
  2. 测试SQL注入:对每个Provider的query、insert、update、delete方法,传入 ' OR 1=1 --" OR "1"="1 等payload
  3. 测试目录遍历:对文件型Provider,尝试 ../../ 路径穿越

小技巧:我常用 drozer 这个工具来测Provider。一条命令就能扫描所有暴露的Provider,还能自动注入payload。不过工具归工具,最终还是要人工确认——自动化工具经常漏掉一些逻辑漏洞。

14.7 总结

Content Provider的安全,说白了就是数据管道的安全。SQL注入、目录遍历、权限泄露,这三个问题只要有一个没处理好,数据就可能被偷走。

我个人习惯在开发阶段就做好三件事:

  • 所有Provider默认 exported="false",需要暴露的才手动打开
  • 所有用户输入都做参数化查询或路径规范化
  • 权限声明用 dangerous 级别,代码里再加一道校验

做到这三点,Provider基本就稳了。剩下的,就是靠测试来验证了。


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