组件安全:Activity安全、Service安全、BroadcastReceiver安全、ContentProvider安全
Android的四大组件,说白了就是应用的骨架。我做了这么多年安全审计,发现大部分漏洞都出在这四个家伙身上。今天咱们就一个一个捋清楚,看看它们各自有哪些坑,又该怎么填。
核心观点:组件安全的核心就一句话——最小暴露原则。不该让别人碰的,就别export;该让别人碰的,做好权限校验。
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 的安全要点有三个:
- 绑定校验:bindService 时一定要检查调用方
- 前台服务权限:Android 9 以后需要 FOREGROUND_SERVICE 权限
- 拒绝服务攻击: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 里的明文密码。所以路径校验一定要做,而且要做在「白名单」基础上——只允许访问指定目录下的文件。
总结一下
四大组件的安全,说到底就是三件事:
- 控制暴露:exported 该关就关,别偷懒
- 校验身份:谁在调用你,你得知道
- 过滤数据:所有外部输入都不可信
做到这三点,大部分组件安全问题都能防住。剩下的就是具体场景下的细节处理了,比如 Service 的 Binder 接口设计、ContentProvider 的 URI 匹配规则等等。这些咱们后面再细聊。
公众号:蓝海资料掘金营,微信deep3321