BroadcastReceiver安全:动态与静态注册的风险、有序广播的权限控制、本地广播与全局广播的选择

BroadcastReceiver,说白了就是Android里的消息喇叭。谁都能发,谁都能收。但问题也出在这里——喇叭太响,容易被坏人利用

我刚开始做Android安全那会儿,总觉得BroadcastReceiver没啥好保护的。直到有一次,我帮一个金融App做审计,发现他们的登录成功广播是全局的。结果呢?任何App都能监听用户什么时候登录了。嗯,这要是被钓鱼App拿到,后果不堪设想。

今天我们就来聊聊,怎么让这个“喇叭”只对正确的人说话。

动态注册 vs 静态注册:谁更安全?

先看两种注册方式。静态注册在AndroidManifest里声明,动态注册在代码里调用registerReceiver。很多人觉得静态注册方便,但方便的背后是风险。

静态注册的问题

  • App安装时,系统就解析了所有静态Receiver。第三方App可以通过PackageManager查到你的Receiver列表。
  • 即使App没在运行,静态Receiver也能被唤醒。这给了攻击者“拉起进程”的机会。
  • 我记得有个案例:某社交App的静态Receiver没加权限,攻击者发送恶意广播,直接让App后台启动,疯狂消耗流量。

动态注册的优势

  • 只在App存活期间有效。进程被杀,广播就收不到了。
  • 可以在代码里动态控制注册时机,比如只在用户登录后才注册。
  • 我个人习惯:能用动态就别用静态。除非你确实需要在App未启动时接收系统广播(比如开机广播)。

核心原则:静态注册暴露面更大,动态注册更可控。能动态就别静态。

有序广播的权限控制

有序广播(Ordered Broadcast)是按优先级依次传递的。每个Receiver可以处理、修改数据,甚至截断广播。这听起来很强大,但也是双刃剑。

为什么会这样?因为有序广播的“有序”只保证顺序,不保证安全。如果某个恶意Receiver优先级高,它可以在你的Receiver之前拿到广播,甚至把数据改了。

我在项目中遇到过:一个支付App使用有序广播传递支付结果。结果有个流氓App注册了更高优先级的Receiver,把支付成功的状态改成了失败。用户付了钱,App却显示支付失败。嗯,这锅谁来背?

解决方案

  • 发送有序广播时,必须指定权限。只有持有对应权限的Receiver才能接收。
  • 接收有序广播时,检查发送者的包名和签名。别信广播里的任何数据。
  • 如果可能,用LocalBroadcastManager代替全局有序广播。

警告:有序广播的“有序”不等于“安全”。权限控制是必须的,否则你的广播就是裸奔。

本地广播 vs 全局广播:怎么选?

这个问题其实很简单:你的广播需要出App吗?

如果不需要,用本地广播。LocalBroadcastManager是Android Support库提供的,只在App内部传递。它不走系统Binder,效率高,而且天然安全——外部App根本收不到。

我见过太多App,明明只是内部组件通信,却用了全局广播。你想想看,这等于在自己家里开派对,却把大门敞开,谁都能进来。何必呢?

选择指南

场景 推荐方式 原因
App内部组件通信 本地广播 安全、高效、无需权限
跨App通信 全局广播 + 权限 必须暴露给外部,但加锁
系统广播(如网络变化) 全局广播(静态注册) 系统只发全局广播,没办法

避坑指南:我曾经把一个内部状态更新写成全局广播,结果测试时发现另一个App也在监听。从那以后,我写广播前都会问自己一句:这个广播需要出App吗?

知识体系总览

下面这张图,是我梳理的BroadcastReceiver安全知识体系。你可以把它当作一个检查清单,每次写广播时对照一下。

BroadcastReceiver安全知识体系 BroadcastReceiver安全 注册方式 有序广播权限 本地 vs 全局 静态注册 动态注册 权限声明 发送者校验 截断防护 LocalBroadcastManager 全局广播+权限 核心原则 最小暴露 · 权限控制 · 本地优先

代码示例:安全的本地广播

说了这么多,上点干货。下面是我常用的本地广播写法,你直接拿去用。

// 发送端
Intent intent = new Intent("com.example.MY_EVENT");
intent.putExtra("data", "some_value");
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

// 接收端
BroadcastReceiver receiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String data = intent.getStringExtra("data");
        // 处理数据
    }
};
IntentFilter filter = new IntentFilter("com.example.MY_EVENT");
LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);

// 别忘了在onDestroy里取消注册
@Override
protected void onDestroy() {
    super.onDestroy();
    LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
}

小技巧:本地广播的IntentFilter action可以随便写,不用加包名前缀。但我习惯加上,方便以后排查问题。

避坑总结

最后,把我这些年踩过的坑总结一下:

  • 别用全局广播做内部通信——这是最常见的坑。本地广播又快又安全,为什么不用?
  • 有序广播一定要加权限——不加权限的有序广播,等于把数据送到别人手里。
  • 动态注册记得取消——忘了unregisterReceiver会导致内存泄漏。我见过一个App,每次进入页面都注册一次,最后Receiver堆了几十个。
  • 静态Receiver的exported属性——如果不需要外部调用,记得设成false。Android 12以后,没指定exported的静态Receiver会报错。

嗯,BroadcastReceiver的安全其实不难。记住一句话:能本地就别全局,能动态就别静态,能加权限就别裸奔。做到这三点,你的广播就安全了一大半。


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