安全编码规范:四大组件安全编码 Checklist

说实话,做 Android 安全这几年,我见过太多漏洞了。有些是架构设计的问题,但更多的——嗯,其实是编码阶段埋下的雷。你想想看,一个 exported="true" 随手一写,可能就把整个应用的数据暴露了。

今天我就把自己多年积累的「四大组件安全编码 Checklist」分享出来。这不是什么官方文档,是我踩坑踩出来的实战经验。每个点我都吃过亏,你照着做,至少能防住 90% 的常见攻击。

核心原则:默认不导出,最小权限原则,数据校验不信任。
四大组件安全编码 Checklist Activity exported + intent 校验 Service 权限 + 隐式意图 BroadcastReceiver 本地广播 + 权限 ContentProvider URI 权限 + 路径校验 关键防护措施 ✅ 默认 exported="false" ✅ 输入数据严格校验 ✅ 使用自定义权限保护 ✅ 日志不打印敏感信息 ✅ 最小化 data 暴露范围 ✅ 使用 LocalBroadcastManager

一、Activity 安全编码 Checklist

Activity 是最容易被攻击的组件。为什么?因为它直接跟用户交互,攻击者只要构造一个恶意 Intent 就能启动你的页面。我在项目中遇到过好几次,第三方 SDK 的 Activity 被恶意调起,直接弹出了钓鱼界面。

检查项 说明 优先级
exported 显式设置 所有 Activity 必须显式声明 exported,不要依赖默认值 P0
Intent 数据校验 对 getIntent() 获取的数据做非空和类型校验 P0
隐式 Intent 过滤 如果使用 intent-filter,必须设置 android:exported="false" 或加权限 P1
Fragment 注入防护 避免使用 Fragment 动态加载不可信类 P1
Task 劫持防护 关键页面设置 android:taskAffinity="" 和 FLAG_ACTIVITY_NEW_TASK P2
我的习惯:每个 Activity 的 onCreate 里,第一件事就是校验 Intent 来源。写个 checkIntentSource() 方法,养成肌肉记忆。

二、Service 安全编码 Checklist

Service 的问题往往更隐蔽。它不像 Activity 那样有 UI,攻击者启动了你的 Service,你甚至都不知道。我记得有一次,某个后台下载 Service 被恶意调用了上千次,直接把用户流量耗光了。

检查项 说明 优先级
exported 控制 内部 Service 必须 exported="false",跨进程 Service 加权限 P0
权限校验 onBind/onStartCommand 中校验调用方权限 P0
隐式 Intent 禁止 Android 5.0+ 禁止隐式启动 Service,但需显式指定包名 P1
返回值安全 onStartCommand 返回值不要随意使用 START_STICKY P1
Binder 接口防护 AIDL 接口参数做边界校验,防止内存耗尽 P2
我曾经踩过的坑:有个音乐播放 Service,onStartCommand 返回了 START_STICKY。结果被恶意应用反复杀死再重启,导致手机发烫。后来改成 START_NOT_STICKY 才解决。

三、BroadcastReceiver 安全编码 Checklist

BroadcastReceiver 是四大组件里最容易被忽视的。攻击者可以发送恶意广播来触发你的 Receiver,甚至通过有序广播拦截数据。说白了,它就是 Android 组件里的「软肋」。

检查项 说明 优先级
本地广播优先 应用内通信必须使用 LocalBroadcastManager P0
权限保护 动态注册时指定权限,静态注册设置 exported="false" P0
有序广播安全 处理有序广播时,不要信任前序 Receiver 的数据 P1
粘性广播禁用 不要使用 sendStickyBroadcast,数据会持久化 P1
onReceive 耗时控制 onReceive 必须在 10 秒内返回,否则 ANR P2
核心原则:能用 LocalBroadcastManager 就别用全局广播。你想想看,应用内的事情,干嘛要让全世界知道?

四、ContentProvider 安全编码 Checklist

ContentProvider 是数据泄露的重灾区。我见过太多应用直接把数据库暴露出去,连个权限都不加。攻击者只要知道 URI,就能把用户数据拖个精光。

检查项 说明 优先级
exported 控制 内部 Provider 必须 exported="false" P0
URI 权限控制 使用 grantUriPermission 细粒度控制 P0
路径校验 对传入的 URI 做路径遍历防护,防止 ../ 攻击 P0
SQL 注入防护 使用参数化查询,禁止拼接 SQL P0
数据最小化 只暴露必要字段,敏感字段用 null 替代 P1
读写分离 读操作和写操作使用不同的权限级别 P1
我的建议:写 ContentProvider 时,先问自己三个问题:这个数据真的需要暴露吗?暴露给谁?暴露到什么程度?如果答案不明确,就别暴露。

五、通用安全编码规范

除了组件特有的检查项,还有一些通用规范。这些是我在代码审查时必查的点,你最好也记下来。

// 通用安全编码示例
public class SecurityUtils {
    
    // 1. Intent 数据校验
    public static String safeGetStringExtra(Intent intent, String key) {
        if (intent == null || !intent.hasExtra(key)) {
            return "";
        }
        String value = intent.getStringExtra(key);
        return value != null ? value : "";
    }
    
    // 2. 路径遍历防护
    public static boolean isValidPath(String basePath, String targetPath) {
        File base = new File(basePath);
        File target = new File(base, targetPath);
        try {
            String canonicalBase = base.getCanonicalPath();
            String canonicalTarget = target.getCanonicalPath();
            return canonicalTarget.startsWith(canonicalBase);
        } catch (IOException e) {
            return false;
        }
    }
    
    // 3. 权限校验
    public static boolean checkCallingPermission(Context context, String permission) {
        int result = context.checkCallingPermission(permission);
        return result == PackageManager.PERMISSION_GRANTED;
    }
}
注意:不要相信任何来自 Intent 的数据。攻击者可以伪造 Intent 的所有字段,包括包名、Action、Data 等。你想想看,如果连包名都能伪造,那还有什么可信的?

六、Checklist 使用建议

光有 Checklist 还不够,你得把它用起来。我个人的做法是:

  1. 编码前:把 Checklist 贴在工位旁边,写代码时对照着看
  2. 代码审查:每次 MR 必须过一遍 Checklist,逐项确认
  3. 自动化检测:用 lint 或自定义规则,把部分检查项自动化
  4. 定期复盘:每季度回顾一次安全漏洞,更新 Checklist

说实话,安全编码不是什么高深的技术。它更像是一种习惯,一种肌肉记忆。你坚持做三个月,后面就变成条件反射了。

最后说一句:安全不是加个权限就完事了。它是一个持续的过程,从编码开始,到上线,再到运维,每个环节都不能放松。我见过太多「加个权限就以为安全了」的案例,结果被绕过得干干净净。

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