组件安全:Activity安全、Service安全、BroadcastReceiver安全、ContentProvider安全

Android的四大组件,说白了就是应用的骨架。我做了这么多年安全审计,发现大部分漏洞都出在这四个家伙身上。今天咱们就一个一个捋清楚,看看它们各自有哪些坑,又该怎么填。

核心观点:组件安全的核心就一句话——最小暴露原则。不该让别人碰的,就别export;该让别人碰的,做好权限校验。

Android 四大组件安全知识体系 四大组件安全 Activity exported 控制 Intent 劫持 任务栈攻击 Service 前台服务权限 绑定校验 拒绝服务攻击 BroadcastReceiver 动态注册 权限广播 粘性广播风险 ContentProvider SQL 注入 目录遍历

Activity 安全:别让你的界面被人偷了

Activity 是用户直接看到的组件,所以安全问题往往跟「界面劫持」和「未授权访问」有关。我见过最典型的案例——某个银行应用的登录页面,exported 设为 true,结果被恶意应用直接拉起,伪造了一个一模一样的界面。

⚠️ 常见风险:

  • exported 滥用:不需要被外部调用的 Activity 也暴露出去
  • Intent 劫持:隐式 Intent 可能被恶意应用拦截
  • 任务栈攻击:恶意 Activity 插入到你的任务栈中

怎么防?其实很简单。先问自己一个问题:这个 Activity 需要被其他应用启动吗?

不需要?那就在 AndroidManifest 里明确写上:

<activity
    android:name=".MainActivity"
    android:exported="false" />

需要?那就做好权限校验。我个人习惯的做法是:

// 在 onResume 中校验调用方身份
@Override
protected void onResume() {
    super.onResume();
    if (isCalledFromExternalApp()) {
        // 检查调用方包名是否在白名单中
        String callerPkg = getCallingPackage();
        if (!isTrustedPackage(callerPkg)) {
            finish(); // 直接关掉
        }
    }
}

💡 我的经验:曾经有个项目,Activity 用了隐式 Intent 来接收外部图片。结果上线后发现有竞品应用拦截了这个 Intent,用户选图时直接跳到了竞品的界面。后来我们改成了显式 Intent + 包名校验,问题才解决。

Service 安全:后台的守护者也要上锁

Service 在后台默默干活,容易被忽视。但恰恰是这种「没人管」的状态,最容易出问题。

Service 的安全要点有三个:

  1. 绑定校验:bindService 时一定要检查调用方
  2. 前台服务权限:Android 9 以后需要 FOREGROUND_SERVICE 权限
  3. 拒绝服务攻击:Service 崩溃会导致整个应用受影响

来看一个绑定校验的代码示例:

@Override
public IBinder onBind(Intent intent) {
    // 检查调用方包名
    String callerPkg = getPackageManager()
        .getNameForUid(Binder.getCallingUid());
    
    if (!"com.trusted.app".equals(callerPkg)) {
        return null; // 拒绝绑定
    }
    return binder;
}

关键点:Service 的 onBind 方法返回 null 时,调用方会得到一个空 Binder,这比直接抛异常要优雅得多。

我记得有一次做安全审计,发现某个支付应用的 Service 没有做任何调用方校验。任何应用都能绑定上去,然后通过 Binder 接口获取交易数据。嗯,这个漏洞评级直接给了「高危」。

BroadcastReceiver 安全:广播不是你想发就能发

BroadcastReceiver 的问题往往出在「过度接收」和「随意发送」上。说白了,就是广播的权限控制没做好。

我把它分成两种情况:

场景 风险 解决方案
发送广播 敏感数据被其他应用接收 使用权限广播或显式广播
接收广播 接收恶意广播导致异常 校验发送方权限

发送安全广播的正确姿势:

// 方式一:使用权限广播
sendBroadcast(intent, "com.example.permission.MY_BROADCAST");

// 方式二:使用显式广播(Android 8.0 以后推荐)
Intent intent = new Intent("com.example.MY_ACTION");
intent.setPackage("com.example.targetapp");
sendBroadcast(intent);

接收广播时也要留个心眼:

// 动态注册时指定权限
registerReceiver(receiver, filter, "com.example.permission.MY_BROADCAST", null);

// 或者在 onReceive 中校验
@Override
public void onReceive(Context context, Intent intent) {
    // 检查发送者权限
    if (checkCallingPermission("com.example.permission.MY_BROADCAST") 
        != PackageManager.PERMISSION_GRANTED) {
        return;
    }
    // 处理广播...
}

⚠️ 特别注意:Android 8.0 以后,静态注册的隐式广播大部分被禁用了。但动态注册的广播仍然存在风险。我建议能用 LiveData 或 EventBus 替代的,就别用广播了。

ContentProvider 安全:数据的大门要守好

ContentProvider 是四大组件里最容易出「数据泄露」问题的。你想想看,它直接暴露了数据库的读写接口,一旦权限没设好,等于把数据库密码贴在大门上。

常见的攻击方式:

  • SQL 注入:通过拼接查询语句注入恶意 SQL
  • 目录遍历:通过 openFile 接口读取任意文件
  • 权限绕过:利用临时权限机制绕过检查

先看 SQL 注入怎么防:

// 错误写法(千万别这么干)
Cursor cursor = db.rawQuery(
    "SELECT * FROM users WHERE id = " + uri.getLastPathSegment(), 
    null);

// 正确写法(使用参数化查询)
Cursor cursor = db.query(
    "users",
    null,
    "id = ?",
    new String[]{uri.getLastPathSegment()},
    null, null, null);

再看文件访问的安全控制:

@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) 
    throws FileNotFoundException {
    
    // 检查路径是否合法(防止目录遍历)
    String path = uri.getLastPathSegment();
    if (path.contains("..") || path.contains("/")) {
        throw new SecurityException("非法路径");
    }
    
    File file = new File(getContext().getFilesDir(), path);
    return ParcelFileDescriptor.open(file, 
        ParcelFileDescriptor.MODE_READ_ONLY);
}

💡 避坑指南:我曾经遇到一个 ContentProvider,它用 openFile 接口暴露了应用私有目录下的文件。攻击者通过构造 ../../shared_prefs/xxx.xml 这样的路径,直接读走了 SharedPreferences 里的明文密码。所以路径校验一定要做,而且要做在「白名单」基础上——只允许访问指定目录下的文件。

总结一下

四大组件的安全,说到底就是三件事:

  1. 控制暴露:exported 该关就关,别偷懒
  2. 校验身份:谁在调用你,你得知道
  3. 过滤数据:所有外部输入都不可信

做到这三点,大部分组件安全问题都能防住。剩下的就是具体场景下的细节处理了,比如 Service 的 Binder 接口设计、ContentProvider 的 URI 匹配规则等等。这些咱们后面再细聊。


公众号:蓝海资料掘金营,微信deep3321