安全编码规范:四大组件安全编码 Checklist
说实话,做 Android 安全这几年,我见过太多漏洞了。有些是架构设计的问题,但更多的——嗯,其实是编码阶段埋下的雷。你想想看,一个 exported="true" 随手一写,可能就把整个应用的数据暴露了。
今天我就把自己多年积累的「四大组件安全编码 Checklist」分享出来。这不是什么官方文档,是我踩坑踩出来的实战经验。每个点我都吃过亏,你照着做,至少能防住 90% 的常见攻击。
核心原则:默认不导出,最小权限原则,数据校验不信任。
一、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 还不够,你得把它用起来。我个人的做法是:
- 编码前:把 Checklist 贴在工位旁边,写代码时对照着看
- 代码审查:每次 MR 必须过一遍 Checklist,逐项确认
- 自动化检测:用 lint 或自定义规则,把部分检查项自动化
- 定期复盘:每季度回顾一次安全漏洞,更新 Checklist
说实话,安全编码不是什么高深的技术。它更像是一种习惯,一种肌肉记忆。你坚持做三个月,后面就变成条件反射了。
最后说一句:安全不是加个权限就完事了。它是一个持续的过程,从编码开始,到上线,再到运维,每个环节都不能放松。我见过太多「加个权限就以为安全了」的案例,结果被绕过得干干净净。
公众号:蓝海资料掘金营,微信deep3321