我是 akihi,一名甲方网络安全工程师,专注 Web / App / PC 客户端漏洞挖掘,腾讯 SRC 连续三年 TOP10、合合 SRC 2025 年度第一,单个漏洞最高 4w+。下面聊一个我在移动端挖洞时几乎每次都先看一遍的组件——Content Provider。
Content Provider 是 Android 里专门跨应用共享数据的组件,通讯录、短信这类系统数据都靠它暴露。正因为「天生要把数据给别的 App」,它几乎每一条路都可能是攻击面。这篇文章把我见过的六类配置错误整理出来,每条都附上能直接复现的攻击方式。

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-path、files-path、cache-path、external-path 等。
问题出在授权范围。很多 App 图省事,直接把整个 files 或 cache 目录放进去,甚至 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.xml,getLastPathSegment() 解码后拼出 ../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:readPermission 管 query(),android:writePermission 管 insert() / 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 合成一个,结果安全级别被拉低了。最典型的两个:
- 自己的 Provider 用
normal级权限,但内部转发去访问系统dangerous级权限的 Provider(比如通讯录)。攻击者只要申请到这个 normal 权限,就能借道读通讯录。 - 更严重的是动态 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);

修复:每个 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

415

被折叠的 条评论
为什么被折叠?



