组件安全: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 的每个方法入口都做参数合法性检查。

知识体系总览

下面这张图是我自己整理的组件安全知识体系,你可以对照着检查自己的应用:

Android 四大组件安全知识体系 Activity Service BroadcastReceiver ContentProvider 核心安全要点 Activity 安全 • exported=false 默认关闭 • 校验调用来源包名 • 敏感页面加 intent 校验 Service 安全 • 关闭不必要的导出 • 绑定服务加权限声明 • 防止资源耗尽攻击 BroadcastReceiver 安全 • 默认 exported 需注意 • 校验发送者身份 • 优先用 LocalBroadcast ContentProvider 安全 • 读写权限分离控制 • 防止 SQL 注入 • 参数合法性校验 核心原则 最小暴露原则 + 权限控制 + 输入校验 = 组件安全

总结一下

组件安全说白了就三件事:

  • 最小暴露原则:不需要被外部调用的组件,一律关掉 exported
  • 权限控制:必须暴露的组件,用自定义权限保护,protectionLevel 用 signature
  • 输入校验:所有外部传入的数据都不信任,做合法性检查

我做了这么多年安全,发现大部分漏洞都是因为开发者图省事,忘了关 exported,或者忘了做校验。你只要养成习惯,每次写组件都问自己一句:「这个组件需要被外部调用吗?」——就能避免 80% 的安全问题。

最后分享一个小技巧:写代码的时候,把 AndroidManifest 里所有组件的 exported 属性都显式写出来,哪怕默认是 false。这样代码审查的时候一目了然,不会漏掉。

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