contentProvider的call方法安全风险详解

84 阅读5分钟

android的ContentProvider 是用来在不同应用之间共享数据,它就像一个“数据中转站”,提供统一的接口(URI)让其它 App 读取或写入本应用托管的数据

ContentProvider 的标准操作包括以下四类:

  • query() :查询数据(只读操作)

  • insert() :插入数据

  • update() :更新数据

  • delete() :删除数据

这四个方法构成了 ContentProvider 的标准 CRUD 能力,也是 Android 系统能够进行权限管控的操作范围。

除了这些标准操作外,Android还提供了ContentProvider.call(String method, String arg, Bundle extras),它是一个 通用 RPC(远程过程调用)接口,允许不同应用之间调用自定义功能。

它不属于标准的 CRUD(query / insert / update / delete),而是:

把应用内部的一段逻辑封装成一个“方法”对外开放,让别的 App 来执行。

1️⃣ 为什么会有 call()?

✔ 使用场景

“我想对外暴露某个能力,但不想把整张表/整块数据库都开放。”

典型例子:

  • 相册:generateShareLink() 生成照片分享链接
  • 通讯录:syncContacts() 触发同步任务

这些都是功能调用而非标准的 query/insert/update/delete。

2️⃣ call() 的工作方式

✔ 完全自定义 method 名称

比如:

  • generateToken

  • doPayment

  • getCardInfo

  • bindSim

  • getTrafficCardList

系统 不会校验这些方法是否危险,也不知道它们的作用

✔ 参数和返回值都通过 Bundle 传输

代码示例

public class DemoProvider extends ContentProvider {

    @Override public boolean onCreate() { return true; }
    @Override
    //此处的call方法是实现一个名为 "ping" 的自定义远程调用方法,并返回一个简单的响应
    public Bundle call(String method, String arg, Bundle extras) {
        if ("ping".equals(method)) {
            Bundle out = new Bundle();
            out.putString("msg", "pong-from-provider");
            return out;
        }
        return super.call(method, arg, extras);
    }

call方法相比较CRUD,灵活性高得多。

3️⃣ call() 为什么如此容易出漏洞?

⚠原因:Android 框架完全不知道 call() 要干什么,也无法做权限控制

和标准方法相对比:

操作类型系统能否强制权限检查?受哪些权限约束?
query / openFile✔ 能检查读权限android:readPermission
insert / update / delete✔ 能检查写权限android:writePermission
call()✔可以,但是需要合理的设置通过manifest合理的权限设置或者在call方法内部进行权限鉴定

对call方法,只要满足下面三个条件就会导致任何app都能随意调用call():

  1. android:exported="true"
  2. Provider在manifest中没有配置任何权限
  3. call() 内部没有做鉴权

4️⃣ 官方文档也明确警告过

Android 官方文档对 call() 有一句非常关键的警告:

系统不会对 call() 做任何权限检查,开发者必须在内部自行鉴权,否则会被未授权应用调用。

image.png

来源:developer.android.com/reference/a…

5️⃣ 测试call方法的权限保护

这里我们准备两个app,一个是提供call方法的app,一个是调用这个call方法的app,模拟任意app,注意两个app的签名要设置成不一样。它们的设置分别如下:

-》提供call方法的app

  • call方法不做任何鉴权(这里在测试时一直不变)
public class DemoProvider extends ContentProvider {

    @Override public boolean onCreate() { return true; }

    // --- 核心:call 不做任何鉴权 ---
    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        if ("ping".equals(method)) {
            Bundle out = new Bundle();
            out.putString("msg", "pong-from-provider");
            return out;
        }
        return super.call(method, arg, extras);
    }
}
  • 在manifest中声明的权限(这里就是测试设置的变量)
<!-- 声明两个权限(设为 signature 更能说明问题:第三方根本拿不到) -->
<permission
        android:name="com.demo.perm.READ_DEMO"
        android:protectionLevel="signature"/>
<permission
        android:name="com.demo.perm.WRITE_DEMO"
        android:protectionLevel="signature"/>
<permission
        android:name="com.demo.perm.global"
        android:protectionLevel="signature"/>

<application android:allowBackup="false" android:label="ProviderApp">
  
    <provider
            android:name=".DemoProvider"
            android:authorities="com.demo.providerapp.demo"
            android:exported="true"
            //测试过程中将会分别对permission、readPermission、writePermission进行设置
            android:permission="com.demo.perm.global"
            android:readPermission="com.demo.perm.READ_DEMO"
            android:writePermission="com.demo.perm.WRITE_DEMO"
            android:grantUriPermissions="false"/>
</application>

-》调用call方法的App

  • 权限申请 首先把提供call方法的app需要的权限都申请一遍(但其实和对方签名不一样,都是无法申请成功的)
<uses-permission android:name="com.demo.perm.READ_DEMO" />
<uses-permission android:name="com.demo.perm.WRITE_DEMO" />
<uses-permission android:name="com.demo.perm.global" />

<application
        android:allowBackup="false"
        android:label="CallerApp"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">

    <activity
            android:name=".MainActivity"
            android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

</application>
  • 跨进程调用call方法
private void doCall() {
    tvResult.setText("正在调用 Provider.call("ping") ...");

    Bundle extras = new Bundle();
    extras.putString("msg", "hello from CallerApp");

    try {
        // method = "ping",arg 暂时传 null,extras 里放点测试数据
        Bundle result = getContentResolver().call(
                DEMO_URI,
                "getTrafficCardConfig",
                null,
                extras
        );

        if (result == null) {
            String msg = "调用成功,但返回 Bundle 为 null(可能 Provider 没有 put result)";
            tvResult.setText(msg);
            Log.i(TAG, msg);
        } else {
            // 假设你在 DemoProvider.call 里用 key "reply" 返回字符串
            String reply = result.getString("trafficCardConfig");
            String msg = "调用成功,返回 reply = " + result;
            tvResult.setText(msg);
            Log.i(TAG, msg);
        }
    } catch (SecurityException se) {
        String msg = "SecurityException:权限不允许调用 call():\n" + se;
        tvResult.setText(msg);
        Log.e(TAG, msg, se);
    } catch (Throwable t) {
        String msg = "其他异常:\n" + t;
        tvResult.setText(msg);
        Log.e(TAG, msg, t);
    }
}

-》测试结果

提供call方法的app的contentProvider权限设置调用的call方法的app调用结果测试结论
仅设置全局权限image.png无法调用成功image.png安全
仅设置写权限 image.png可以调用成功image.png不安全
仅设置读权限 image.png可以调用成功!image.png不安全
不设置全局,同时设置读和写权限 image.png无法调用成功!image.png安全

6️⃣小结

由上述测试结果我们知道,要保护call方法,如果是在manifest中设置权限的话,只有以下两种是安全的,受到权限设置的保护:

  • 设置permission权限,同时将该权限声明为signature级,这样只有与本应用 同签名的 APK 才能调用包括 call() 在内的所有入口
  • 同时设置readPermission和writePermission,同时将他们的权限声明为signature级。
  • 另外请注意:readPermission、writePermission优先级比permission高,如果混用请注意优先级。

除此之外,如果不在manifest中保护的话,还可以在call方法的内部进行权限保护,例如:

@Override
public Bundle call(String method, String arg, Bundle extras) {
    // 1. 统一做权限校验(使用 signature 级自定义权限)
    final String requiredPerm = "com.demo.app.PERM_SECURE_CALL";
    final int uid = Binder.getCallingUid();
    // 如果调用方没有声明/授予requiredPerm(本app自定义权限名称)权限,将直接抛出 SecurityException
    getContext().enforceCallingPermission(
            requiredPerm,
            "Permission denied for uid=" + uid + " to call() on SecureProvider");

    // 2. 可选:再按包名做一层白名单校验,进一步收紧调用范围
    String callingPkg = getCallingPackage(); // 某些版本可能为 null,可结合 UID 反查
    if (!isTrustedCaller(callingPkg)) {
        throw new SecurityException("Untrusted caller package: " + callingPkg);
    }

    // 3. 根据 method 分发具体能力
    if ("ping".equals(method)) {
        Bundle out = new Bundle();
        out.putString("msg", "pong-from-provider");
        return out;
    } else if ("getTrafficCardConfig".equals(method)) {
        // TODO: 这里执行真实业务逻辑
        return getTrafficCardConfig();
    }

    // 4. 未知方法直接拒绝,避免被枚举/探测
    throw new IllegalArgumentException("Unknown call() method: " + method);
}

private boolean isTrustedCaller(String pkg) {
    if (pkg == null) return false;
    // 简单示例:只允许同家族应用调用
    return "com.demo.app".equals(pkg)
            || "com.demo.app.partner".equals(pkg);
}