组件安全:Activity、Service、BroadcastReceiver、ContentProvider
Android 的四大组件,说白了就是应用的四个「入口」。我做了这么多年安全审计,发现大部分漏洞都出在这几个入口上。你想想看,每个组件都可能被外部应用调用,一旦暴露了不该暴露的东西,数据泄露、权限提升、拒绝服务攻击就都来了。
今天咱们就把这四个组件挨个捋一遍。我会结合我踩过的坑,告诉你每个组件该怎么安全开发。
Activity 安全
Activity 是用户直接交互的界面。但很多人不知道,它也能被外部应用偷偷启动。
核心风险:Activity 被劫持、敏感界面被外部启动、返回栈攻击
我个人习惯,所有不需要被外部调用的 Activity,都在 AndroidManifest 里把 exported 设为 false。这是最基础的一步。
<activity
android:name=".MainActivity"
android:exported="false" />
如果确实需要暴露给外部,比如分享功能,那就要做权限校验。我在项目中遇到过一个问题:某个支付确认页面被恶意应用直接启动了,用户都没经过下单流程就跳到了支付页。后来怎么修的?加了个 intent 参数校验。
// 检查调用来源是否合法
if (!isCallerTrusted(getCallingActivity())) {
finish();
return;
}
我的建议:敏感操作页面(登录、支付、修改密码)一定要检查 intent 的来源。可以用 getCallingActivity() 获取调用者包名,跟白名单比对。
Service 安全
Service 在后台运行,容易被忽略。但攻击者可以通过绑定 Service 来获取敏感数据,或者通过启动 Service 造成资源耗尽。
说白了,Service 的安全原则跟 Activity 一样:不需要暴露的就关掉 exported。
<service
android:name=".DataSyncService"
android:exported="false" />
如果 Service 需要被外部绑定,记得做权限声明。我曾经见过一个案例:某个应用的定位 Service 被恶意应用绑定了,结果用户的行踪被实时窃取。修复方案就是加了个自定义权限。
<permission
android:name="com.example.permission.BIND_LOCATION_SERVICE"
android:protectionLevel="signature" />
<service
android:name=".LocationService"
android:permission="com.example.permission.BIND_LOCATION_SERVICE" />
注意:protectionLevel 设为 signature 意味着只有相同签名的应用才能访问。这是最安全的做法之一。
BroadcastReceiver 安全
BroadcastReceiver 是四大组件里最容易出问题的。为什么?因为它默认就是导出的!
嗯,这里要注意:Android 8.0 以后,静态注册的 BroadcastReceiver 默认 exported 为 true。如果你不处理,任何应用都能给你发广播。
<receiver
android:name=".MyReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.example.MY_ACTION" />
</intent-filter>
</receiver>
如果必须接收外部广播,一定要校验发送者的身份。我习惯在 onReceive 里加个包名检查:
@Override
public void onReceive(Context context, Intent intent) {
String callerPackage = getCallingPackage();
if (!"com.trusted.app".equals(callerPackage)) {
return;
}
// 处理广播
}
避坑指南:有序广播可以设置权限,让只有持有特定权限的应用才能发送。无序广播则建议用 LocalBroadcastManager 替代,它只在应用内部传播,安全得多。
ContentProvider 安全
ContentProvider 是数据共享的桥梁。但很多人把它当成了「数据后门」。我见过最离谱的一个案例:某个应用的数据库直接通过 ContentProvider 暴露了,连密码都没设。
ContentProvider 默认 exported 为 true(如果定义了
<provider
android:name=".MyProvider"
android:exported="false" />
如果确实需要共享数据,用权限控制:
<provider
android:name=".UserProvider"
android:readPermission="com.example.permission.READ_USER"
android:writePermission="com.example.permission.WRITE_USER"
android:exported="true" />
还有一个很多人忽略的点:SQL 注入。ContentProvider 的 query 方法如果直接拼接参数,很容易被注入。
// 错误写法
Cursor cursor = db.rawQuery("SELECT * FROM users WHERE id = " + id, null);
// 正确写法
Cursor cursor = db.query("users", null, "id = ?", new String[]{id}, null, null, null);
警告:永远不要相信外部传入的数据。所有参数都要做校验和转义。我习惯在 ContentProvider 的每个方法入口都做参数合法性检查。
知识体系总览
下面这张图是我自己整理的组件安全知识体系,你可以对照着检查自己的应用:
总结一下
组件安全说白了就三件事:
- 最小暴露原则:不需要被外部调用的组件,一律关掉 exported
- 权限控制:必须暴露的组件,用自定义权限保护,protectionLevel 用 signature
- 输入校验:所有外部传入的数据都不信任,做合法性检查
我做了这么多年安全,发现大部分漏洞都是因为开发者图省事,忘了关 exported,或者忘了做校验。你只要养成习惯,每次写组件都问自己一句:「这个组件需要被外部调用吗?」——就能避免 80% 的安全问题。
最后分享一个小技巧:写代码的时候,把 AndroidManifest 里所有组件的 exported 属性都显式写出来,哪怕默认是 false。这样代码审查的时候一目了然,不会漏掉。