ContentProvider 数据泄露原理:Provider 的导出属性、URI 匹配漏洞、SQL 注入风险
好,咱们今天来聊聊 ContentProvider 的安全问题。说实话,这个组件在 Android 里经常被忽视,但一旦出问题,就是数据裸奔级别的灾难。我见过太多应用把数据库直接暴露给外界,还浑然不觉。
ContentProvider 的设计初衷是好的——让应用之间安全地共享数据。但问题是,很多人把它当成了「随便访问的数据仓库」。你想想看,一个没加锁的保险柜,谁路过都能翻一翻,这能安全吗?
1. Provider 的导出属性:你家的门到底锁没锁?
先说最基础的问题——导出属性。ContentProvider 在 AndroidManifest.xml 里有个 android:exported 属性。这个值决定了其他应用能不能找到并访问你的 Provider。
<intent-filter>,系统会默认把 exported 设为 true。很多人不知道这个坑,直接导致数据泄露。
来看一个典型的错误配置:
<provider
android:name=".MyDataProvider"
android:authorities="com.example.app.data"
android:exported="true" <!-- 这就是问题所在 -->
android:grantUriPermissions="false" />
嗯,这里要注意。把 exported 设为 true 意味着任何第三方应用都能通过 ContentResolver 来查询你的数据。我在项目中遇到过一家金融类 App,他们的用户交易记录就是这么泄露的。攻击者只需要写一个简单的 App,调用 getContentResolver().query() 就能拿到所有数据。
正确的做法是:
- 如果 Provider 仅供本应用使用,设置
android:exported="false" - 如果需要跨应用共享,使用
android:permission限定访问权限 - 或者使用
android:readPermission和android:writePermission分别控制读写
exported="false",等确实需要共享数据时,再按最小权限原则开放。别一开始就图省事。
2. URI 匹配漏洞:路径上的陷阱
好,假设你正确地设置了 exported="false",但如果你需要对外提供数据,就得用 URI 匹配。这里又是个容易翻车的地方。
ContentProvider 使用 UriMatcher 来匹配请求的 URI。如果匹配规则写得不够严谨,攻击者可以通过构造特殊的 URI 来访问本不该暴露的数据。
举个例子:
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI("com.example.app.data", "users", USERS);
sUriMatcher.addURI("com.example.app.data", "users/#", USER_ID);
sUriMatcher.addURI("com.example.app.data", "users/*/profile", USER_PROFILE);
}
看起来没问题对吧?但如果你在 query() 方法里这样写:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (sUriMatcher.match(uri)) {
case USERS:
// 查询所有用户
break;
case USER_ID:
// 查询单个用户
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
这里有个隐藏问题——如果攻击者传入 content://com.example.app.data/users/../../secret 这样的 URI,你的 UriMatcher 可能匹配不到,但底层数据库却可能被路径遍历攻击。说白了,就是 URI 的解析和数据库的解析不一致,给了攻击者可乘之机。
query()、insert()、update()、delete() 方法里,都要对 URI 做二次校验。
我自己的做法是:在匹配到 URI 后,再检查一下路径的合法性。比如检查是否包含 .. 或 // 等特殊字符。虽然麻烦点,但安全这东西,多一道防线总没错。
3. SQL 注入风险:老生常谈但屡教不改
说到 SQL 注入,大家可能觉得这是 Web 开发的事。但在 Android 的 ContentProvider 里,SQL 注入同样致命。
来看一个典型的漏洞代码:
@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);
}
攻击者只需要传入 ' OR '1'='1 作为查询参数,就能拿到所有用户数据。更狠一点,可以传入 '; DROP TABLE users; --,直接删表。
为什么会这样?说白了,就是没有把用户输入和数据查询语句分开。正确的做法是使用参数化查询:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 安全!使用参数化查询
return db.query("users", projection, "name = ?",
new String[]{selectionArgs[0]}, null, null, sortOrder);
}
这里要注意,db.query() 方法会自动对参数进行转义,防止 SQL 注入。但如果你用了 rawQuery(),就得自己处理参数。
rawQuery() 更灵活,就全部用它。结果被安全团队扫出来一堆 SQL 注入漏洞。后来全部改成 query() 方法,问题才解决。所以我的建议是:能用 query() 就别用 rawQuery()。
4. 知识体系总览
为了让你更直观地理解 ContentProvider 数据泄露的三大风险,我画了一张图:
5. 实战中的防护建议
说了这么多漏洞,咱们来总结一下怎么防。我根据自己的经验,整理了几个要点:
- 严格控制导出属性:非必要不导出。如果必须导出,用
android:permission或自定义权限控制。 - URI 校验要严格:在
query()等方法里,对 URI 做二次校验。别只依赖UriMatcher。 - 永远使用参数化查询:别碰
rawQuery(),除非你百分百确定参数是安全的。 - 最小化数据暴露:只返回必要的字段,别把整个表都暴露出去。
- 日志和监控:记录异常的查询请求,及时发现攻击行为。
openFile() 方法直接下载整个数据库文件。这种问题,光靠代码审查很难发现,得结合安全测试才行。
好了,关于 ContentProvider 的数据泄露原理,咱们就聊到这儿。记住一句话:数据无小事,安全无侥幸。每一个看似不起眼的配置错误,都可能成为攻击者的突破口。