Content Provider 的 6 个安全坑:路径穿越、权限绕过和 SQL 注入

我是 akihi,一名甲方网络安全工程师,专注 Web / App / PC 客户端漏洞挖掘,腾讯 SRC 连续三年 TOP10、合合 SRC 2025 年度第一,单个漏洞最高 4w+。下面聊一个我在移动端挖洞时几乎每次都先看一遍的组件——Content Provider。

Content Provider 是 Android 里专门跨应用共享数据的组件,通讯录、短信这类系统数据都靠它暴露。正因为「天生要把数据给别的 App」,它几乎每一条路都可能是攻击面。这篇文章把我见过的六类配置错误整理出来,每条都附上能直接复现的攻击方式。

Content Provider 攻击面

Content Provider 干的事大致分三类,攻击点也对应这三类:

  • 数据共享query() / insert() / update() / delete() 代理到 SQLite,风险是 SQL 注入、越权读数据。
  • 文件访问openFile() 返回 ParcelFileDescriptor,风险是路径穿越、任意文件读写。
  • 执行动作call() 跑自定义逻辑,风险是触发调试、加解密这类危险动作。

一、FileProvider 授权范围过大

新版 Android 因为 SELinux 限制,App 之间没法直接互相读文件(哪怕 chmod 777)。Google 给的方案是 FileProvider:用 android:grantUriPermissions="true" 把特定文件临时授权给别的 App。它本身不能导出,靠一个 XML 配置声明哪些路径可以共享,可选的有 root-pathfiles-pathcache-pathexternal-path 等。

问题出在授权范围。很多 App 图省事,直接把整个 filescache 目录放进去,甚至 root-path 整个 /。一旦这个 FileProvider 和一个「能把请求重定向进来的漏洞」串起来,攻击者就能借它的授权读/写任意文件。Samsung 上就有过这么一条链,最终拿到了 system 权限下的任意文件读写。

修复:一个 FileProvider 只干一件事(比如只共享图片),授权目录收窄到具体子目录,比如 /data/user/0/com.victim/cache/my_image_cache/,而不是整个 cache/

二、Uri 路径穿越

处理文件时,开发经常用 Uri.getLastPathSegment()Uri.getPathSegments() 取路径片段。关键点:这些方法会把 URL 编码解码掉..%2F 会变成 ../

下面这段代码看起来没问题,实际就是个任意文件读取:

public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    File file = new File(getContext().getFilesDir(), uri.getLastPathSegment());
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
}

作者的本意是只返回 files 目录里的文件,但攻击者发个 content://com.victim.path_traversal/..%2Fshared_prefs%2Fsecrets.xmlgetLastPathSegment() 解码后拼出 ../shared_prefs/secrets.xml,直接跳出 files 目录。Google 上这条链做到过任意文件读写(也就是任意代码执行)。

路径穿越攻击链

Uri uri = Uri.parse("content://com.victim.path_traversal/..%2Fshared_prefs%2Fsecrets.xml");
try (InputStream in = getContentResolver().openInputStream(uri)) {
    Log.d("evil", new String(in.readAllBytes()));
} catch (IOException e) {}
修复:对拼出来的路径算一次规范化路径(canonical path)再校验,或者对取值重新编码:Uri.encode(uri.getLastPathSegment())

三、readPermission / writePermission 配错

除了 android:permission,Provider 还有两个属性:android:readPermissionquery()android:writePermissioninsert() / update() / delete()。而文件读取的校验,看的是 openFile() 的 mode 参数(只读 / 只写 / 读写)。

这里有个很常见的坑:ContentResolver 在鉴权时只看 mode 的值,但很多 openFile() 实现里根本没看 mode。比如:

public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    return ParcelFileDescriptor.open(new File(uri.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
}

即使这个 Provider 配了只有签名权限能读的 readPermission,攻击者只要传个 mode="w"(写模式),鉴权就按写权限走,而写权限可能压根没设,于是拿到只读权限去读任意文件:

Uri uri = Uri.parse("content://com.victim.protected/data/user/0/com.victim/shared_prefs/secrets.xml");
ParcelFileDescriptor pfd = getContentResolver().openFile(uri, "w", null);
try (InputStream in = new FileInputStream(pfd.getFileDescriptor())) {
    Log.d("evil", new String(in.readAllBytes()));
} catch (IOException e) {}
修复:openFile() 里校验 mode 和权限,别让 read/write 变成裸奔;不确定要哪个权限时,统一落到 android:permission 上。

四、把请求代理到更安全的 Provider(权限降级)

有些开发者会把多个 Provider 合成一个,结果安全级别被拉低了。最典型的两个:

  1. 自己的 Provider 用 normal 级权限,但内部转发去访问系统 dangerous 级权限的 Provider(比如通讯录)。攻击者只要申请到这个 normal 权限,就能借道读通讯录。
  2. 更严重的是动态 URI:直接用 uri.getQueryParameter("uri") 拿到目标再转发。
case PROXY_CODE: {
    Uri newUri = Uri.parse(uri.getQueryParameter("uri"));
    return getContext().getContentResolver().query(newUri, projection, selection, selectionArgs, sortOrder);
}

这么写等于把「这个 App 能访问的所有 Provider」都开放给攻击者了——包括生态 App 和系统 Provider。

修复:别在自己的 Provider 里转发别的 Provider 请求;非做不可的话,目标必须白名单写死,绝不能让攻击者控制目标 URI。

五、敏感数据和非敏感数据放同一个库

这个坑很隐蔽。两个 Provider 用不同的权限保护(比如一个 signature、一个根本没权限),但底层指向同一个 SQLite 数据库,只是表不同。Android 层的权限看起来分得很干净,但数据库层是通的。

攻击者从权限低的那个 Provider 进来,用 SQL 注入跨表读敏感数据:

Uri uri = Uri.parse("content://com.victim.insensitive/");
String where = "1=2 UNION SELECT * FROM Sensitive -- ";
Cursor c = getContentResolver().query(uri, null, where, null, null);

SQL 注入拖敏感数据

修复:每个 Provider 用独立的数据库文件;不同安全级别的 Provider 之间,别让它们基于传入的 URI 互相通信。

六、Provider 里塞了敏感动作

有些 App 会在 Provider 里加额外的业务逻辑,比如调试功能、自动加解密、在指定目录之外建/改文件。这些逻辑堆在一起,就容易出事。

Oversecured 在一个很流行的 App 里见过这种:Provider 里留了一个 debug 入口,query() 匹配到 debug 就调用 debug(),把内部数据库拷到下载目录。这个 Provider 是 exported="false",代码评审认为安全。但这个 App 里有几十处「从外部收 URI 然后调 ContentResolver.query()」的地方——攻击者构造个 content://com.victim.internal/debug,就能让 App 自己执行这个危险动作。

修复:Provider 只当数据和文件的「代理」,别在里面写改数据、建文件、移文件这类逻辑。

写在最后

Content Provider 的洞几乎都能串起来:一个意图重定向拿到授权,一个 FileProvider 范围过大,再一个 Uri 路径穿越,就能从「读一个文件」滚成「读任意文件」。挖移动端的时候,别只看 Activity 和 WebView,Provider 这三条路(数据、文件、动作)每一条都值得过一遍。

参考来源:Oversecured Blog — Content Providers and the potential weak spots they can have
https://oversecured.com/blog/content-providers-and-the-potential-weak-spots-they-can-have
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值