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

Android的四大组件,说白了就是应用的四个"入口"。每个入口都可能被外部应用调用,也可能被恶意利用。我这些年做安全审计,十有八九的漏洞都跟组件暴露有关。今天咱们就一个一个捋清楚。

核心观点:组件安全的核心就一句话——最小暴露原则。不该对外暴露的,坚决不暴露;必须暴露的,做好权限校验。

Android 四大组件安全防护体系 Activity exported=false intent-filter 谨慎 权限校验 隐式意图风险 Service 前台服务通知 权限声明 拒绝隐式启动 Intent 校验 BroadcastReceiver LocalBroadcastManager 权限保护 有序广播 粘性广播禁用 ContentProvider readPermission writePermission Uri 校验 SQL 注入防护 安全防护核心原则 ① 默认不导出(exported=false) ② 必须导出时加权限校验 ③ 对所有输入做合法性校验

4.1 Activity 安全

Activity是用户直接打交道的组件。你想想看,如果一个Activity被恶意应用直接拉起,会发生什么?轻则界面被劫持,重则用户密码被钓鱼。

最常见的坑:在AndroidManifest里给Activity加了,系统就默认把exported设成true了。很多人不知道这个潜规则。

⚠️ 注意:只要Activity包含,exported默认就是true。如果你只想让系统内部调用,务必显式设置exported="false"。

我遇到过这样一个案例:某社交App的"重置密码"Activity加了用于接收系统通知,结果被恶意应用直接拉起,跳过了身份验证环节。嗯,这就是典型的Activity劫持。

安全编码建议:

  • 所有Activity默认设置android:exported="false"
  • 必须暴露的Activity,做好调用方身份校验
  • 敏感操作前检查调用来源
  • 使用getCallingActivity()判断调用者
// 安全示例:校验调用者身份
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    // 检查是谁调用了这个Activity
    ComponentName caller = getCallingActivity();
    if (caller == null || !"com.yourapp.trusted".equals(caller.getPackageName())) {
        // 非法调用,直接关闭
        finish();
        return;
    }
    
    // 正常业务逻辑
    setContentView(R.layout.activity_sensitive);
}

4.2 Service 安全

Service在后台默默干活,容易被忽视。但恰恰是这种"没人注意"的组件,最容易出问题。

Service的安全风险主要有两个:权限提升拒绝服务。恶意应用可以启动你的Service消耗资源,或者通过Service获取敏感数据。

💡 我的习惯:所有Service都先设成exported="false",除非明确需要跨进程调用。而且我建议用IntentService代替普通Service,生命周期更可控。

安全编码要点:

  • Service默认不导出
  • 必须导出的Service,声明android:permission
  • onStartCommand()中校验Intent参数
  • Android 5.0以上禁止隐式启动Service
<!-- AndroidManifest.xml 安全配置 -->
<service
    android:name=".SecureService"
    android:exported="true"
    android:permission="com.yourapp.permission.ACCESS_SECURE_SERVICE">
</service>
// Service 内部校验
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (intent == null || intent.getAction() == null) {
        stopSelf();
        return START_NOT_STICKY;
    }
    
    // 校验Intent中的参数
    String data = intent.getStringExtra("key_data");
    if (data == null || !isValidData(data)) {
        // 参数不合法,拒绝处理
        stopSelf();
        return START_NOT_STICKY;
    }
    
    // 处理业务逻辑
    handleData(data);
    return START_NOT_STICKY;
}

4.3 BroadcastReceiver 安全

BroadcastReceiver是组件安全的重灾区。为什么?因为广播是全局的,任何应用都能发也能收。

我记得有一次帮客户做安全审计,发现他们的动态广播接收器没有做任何权限限制。结果第三方应用不停地发送伪造广播,导致App频繁弹窗,用户体验极差。这还算轻的,更严重的是数据泄露。

广播安全三大原则:

  1. 能用局部广播就别用全局广播——LocalBroadcastManager是你的好朋友
  2. 动态注册比静态注册更安全——生命周期可控
  3. 敏感广播必须加权限——发送和接收都要加

重点:Android 8.0(API 26)开始,静态注册的广播接收器大部分不能用了。这是好事,因为动态注册更安全。但要注意,动态注册记得在不需要时取消注册。

// 安全的广播注册方式
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 校验发送者权限
        if (!hasRequiredPermission(context, intent)) {
            return;
        }
        // 处理广播
        handleBroadcast(intent);
    }
};

// 注册时指定权限
IntentFilter filter = new IntentFilter("com.yourapp.SECURE_ACTION");
registerReceiver(mReceiver, filter, "com.yourapp.permission.RECEIVE_SECURE", null);

// 别忘了取消注册
@Override
protected void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mReceiver);
}

4.4 ContentProvider 安全

ContentProvider是四大组件里最容易出数据泄露的。它直接操作数据,一旦权限没设好,数据库就裸奔了。

我曾经见过一个App,ContentProvider的exported设成了true,但readPermission和writePermission都没配。结果呢?任何App都能读取用户的通讯录和聊天记录。这简直是灾难。

ContentProvider安全配置表:

配置项 说明 推荐值
android:exported 是否允许外部访问 false(除非必须)
android:readPermission 读取权限 自定义签名权限
android:writePermission 写入权限 自定义签名权限
android:grantUriPermissions 临时授权 谨慎使用

⚠️ 避坑指南:我曾经遇到一个案例,开发者在ContentProvider里用了grantUriPermissions="true",但没有做临时权限回收。结果一个恶意App拿到Uri后,在权限过期前疯狂读取数据。记住:临时授权一定要设置有效期。

// ContentProvider 安全实现示例
public class SecureProvider extends ContentProvider {
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // 1. 校验Uri
        if (!isValidUri(uri)) {
            throw new IllegalArgumentException("Invalid URI");
        }
        
        // 2. 参数化查询,防止SQL注入
        String table = getTableName(uri);
        SQLiteDatabase db = getReadableDatabase();
        return db.query(table, projection, selection, selectionArgs, 
                       null, null, sortOrder);
    }
    
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // 校验输入数据
        if (!validateInput(values)) {
            return null;
        }
        // 安全插入
        return super.insert(uri, values);
    }
    
    // Uri校验
    private boolean isValidUri(Uri uri) {
        return uri != null && 
               "content".equals(uri.getScheme()) && 
               "com.yourapp.provider".equals(uri.getAuthority());
    }
}

4.5 组件安全总结

说了这么多,其实核心就三点:

  • 默认不暴露——exported="false"是默认选项
  • 暴露必校验——权限、身份、数据都要验
  • 输入皆有毒——所有外部来的数据都不可信

做安全开发这么多年,我最大的体会是:安全不是加功能,而是减风险。少暴露一个组件,就少一个攻击面。这个道理,越早明白越好。

💡 最后分享一个小技巧:写完代码后,用adb shell dumpsys package com.yourapp查看所有组件的exported状态。这是我每次上线前必做的检查。


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