Content Provider 安全:URI权限、临时权限、SQL注入防护
Content Provider 是 Android 四大组件里最容易被忽视的安全短板。我见过太多应用把数据库直接暴露给外部,还觉得「反正加了权限就安全」。嗯,今天我们就来聊聊 Provider 安全的三个核心问题:URI 权限怎么给、临时权限怎么用、SQL 注入怎么防。
一、URI 权限模型:你给了谁,能干什么
先想一个问题:你的 Provider 里有一张用户表,一张配置表。你希望第三方应用只能读配置表,不能碰用户表。怎么办?
传统的做法是给整个 Provider 加一个 android:permission。但这样太粗了——要么全开,要么全关。实际项目中,我们往往需要更细粒度的控制。
这就是 URI 权限的用武之地。你可以针对不同的 URI 路径,设置不同的权限。
核心思路: 在 <provider> 标签里用 <path-permission> 子标签,为特定路径单独授权。
举个例子:
<provider
android:name=".MyProvider"
android:authorities="com.example.myapp.provider"
android:exported="true">
<path-permission
android:path="/config"
android:permission="android.permission.READ_EXTERNAL_STORAGE"
android:readPermission="android.permission.READ_EXTERNAL_STORAGE" />
<path-permission
android:path="/user"
android:permission="com.example.myapp.permission.READ_USER"
android:readPermission="com.example.myapp.permission.READ_USER" />
</provider>
这样,访问 /config 只需要普通存储权限,而访问 /user 必须申请你自定义的 READ_USER 权限。我在项目中就遇到过这样的场景:一个健康类 App,运动数据可以开放给第三方,但个人健康档案必须严格保护。用 path-permission 就完美解决了。
小提示: 如果某个路径没有匹配到任何 path-permission,系统会回退到 <provider> 级别的权限。所以记得给根路径也设一个默认权限,避免漏网之鱼。
二、临时权限:用完即弃的通行证
有时候,你只想让某个应用「临时」访问一下你的数据。比如用户从相册选了一张图片,然后通过你的 App 分享给好友。这时候,你不需要给好友的 App 申请任何权限——用临时权限就够了。
临时权限的核心机制是 URI 授权。你可以在 Intent 里带上 FLAG_GRANT_READ_URI_PERMISSION 或 FLAG_GRANT_WRITE_URI_PERMISSION,把某个 URI 的访问权临时授予目标组件。
代码示例:
Uri uri = Uri.parse("content://com.example.myapp.provider/temp_data");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
接收方拿到这个 Intent 后,即使没有声明任何权限,也能读取 temp_data 路径下的数据。为什么能这样?因为系统在 Intent 传递过程中,临时给接收方「开了个后门」。
但要注意:这个后门只在接收方处理 Intent 期间有效。一旦处理完毕,权限自动回收。说白了,这就是一个用完即弃的通行证。
我曾经踩过的坑: 临时权限只对 Intent 中指定的 URI 有效。如果你在 Intent 里传了一个 URI,但接收方又通过这个 URI 去查询其他关联数据,那是不行的。系统只授权了那一个 URI,不是整个 Provider。
另外,如果你的 Provider 要支持临时授权,必须在 <provider> 标签里加上:
android:grantUriPermissions="true"
或者用 <grant-uri-permission> 子标签指定哪些路径支持临时授权。我个人习惯用后者,更可控。
三、SQL 注入防护:别让查询变成灾难
Content Provider 底层往往用 SQLite 存储数据。如果你在 query() 方法里直接拼接用户输入的字符串,那就等着被注入吧。
举个例子,错误的写法:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 危险!直接拼接用户输入
String sql = "SELECT * FROM users WHERE name = '" + selectionArgs[0] + "'";
return db.rawQuery(sql, null);
}
攻击者只要传一个 selectionArgs[0] = "' OR '1'='1",就能把整个用户表拖走。你想想看,这有多可怕。
正确的做法是使用参数化查询:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 安全!使用占位符
String sql = "SELECT * FROM users WHERE name = ?";
return db.rawQuery(sql, selectionArgs);
}
或者直接用 SQLiteDatabase.query() 方法,它内部已经做了参数化处理:
return db.query("users", projection, "name = ?", selectionArgs, null, null, sortOrder);
这里有个容易被忽略的点:selection 参数也可能被注入。有些开发者觉得 selection 是固定的,不会出问题。但如果你在 selection 里拼接了用户输入,一样完蛋。
我的建议: 永远不要相信任何来自客户端的字符串。不管是 selection、selectionArgs 还是 projection,都当作恶意输入来处理。用参数化查询,别偷懒。
四、知识体系总览
下面这张图总结了 Content Provider 安全的三个核心维度:
五、实战建议
说了这么多,总结几条我在项目中一直遵守的原则:
- 最小权限原则: 能不给权限就不给。用
path-permission细化到路径,用临时权限细化到 URI。 - 永远参数化: 不管多简单的查询,都用
?占位符。别跟我说「这个字段是固定的」,你永远不知道明天谁会改代码。 - 日志审计: 在 Provider 的
query()、insert()、update()、delete()方法里加上日志,记录谁访问了什么 URI。出了问题能追溯。 - 不要暴露内部 ID: 如果你用自增 ID 作为 URI 的一部分,攻击者可以遍历。考虑用 UUID 或者哈希值。
一个小技巧: 如果你不确定自己的 Provider 有没有漏洞,可以写一个测试 App,不声明任何权限,然后尝试访问你的 Provider。如果成功了,说明你的权限控制有问题。
Content Provider 的安全,说白了就是三个字:控、限、防。控住路径,限住临时访问,防住注入。做到这三点,你的数据就安全了一大半。
公众号:蓝海资料掘金营,微信 deep3321