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安全知识体系。你可以把它当成一个检查清单,测试时对着看,基本不会漏。
14.6 自动化检测思路
手动测Provider确实累,我一般会写点自动化脚本辅助。核心思路就三条:
- 枚举所有暴露的Provider:用
adb shell dumpsys package或者写个App遍历PackageManager.queryContentProviders() - 测试SQL注入:对每个Provider的query、insert、update、delete方法,传入
' OR 1=1 --、" OR "1"="1等payload - 测试目录遍历:对文件型Provider,尝试
../../路径穿越
小技巧:我常用 drozer 这个工具来测Provider。一条命令就能扫描所有暴露的Provider,还能自动注入payload。不过工具归工具,最终还是要人工确认——自动化工具经常漏掉一些逻辑漏洞。
14.7 总结
Content Provider的安全,说白了就是数据管道的安全。SQL注入、目录遍历、权限泄露,这三个问题只要有一个没处理好,数据就可能被偷走。
我个人习惯在开发阶段就做好三件事:
- 所有Provider默认
exported="false",需要暴露的才手动打开 - 所有用户输入都做参数化查询或路径规范化
- 权限声明用
dangerous级别,代码里再加一道校验
做到这三点,Provider基本就稳了。剩下的,就是靠测试来验证了。
公众号:蓝海资料掘金营,微信deep3321