先看结论与判断条件
- 工程判断:Navigation 生成路由容易被按源码包配置的保护清单漏掉;是否纳入 VMP 必须依据最终 DEX、业务职责和回归结果逐项确认。
- Safe Args 或序列化路由的生成产物不承担登录、授权或业务规则,不能当作业务保护点。
- Kotlin 序列化器需要精确 keep 规则才能存活到 VMP 处理阶段,宽泛规则会掩盖错误。
- 深链匹配成功不表示参数可信,需独立验证来源和权限,入口解析代码须与被调用业务函数一起保护。
- 生成类的混淆和 R8 行为会改变最终可达类集合,保护配置必须基于实际构建产物而非源码猜测。
- instrumented test 是验证 VMP 处理后导航组件是否正常工作的必要手段,单一设备通过不意味着全兼容。
- 多模块工程中参数类可能分布在非 app 模块,只保护 app 模块会导致攻击面残留。
- 若因体积只保护核心业务,需在测试中验证深链入口仍受控,并留存例外记录。
为何必须关注 Navigation 生成路由的保护边界
Android Jetpack Navigation 是现代应用业务跳转的核心框架。在使用 Safe Args 插件或 Compose 类型安全路由时,编译器会在构建阶段自动生成参数容器、方向类和路由匹配代码。这些产物通过注解处理器或 Gradle 插件自动产出,其文件名和类名由工具根据导航图 ID 拼接而成。开发人员日常编码中几乎不会直接编辑这些文件,导致常规加固方案极易遗漏这些自动生成的代码,使其成为潜在的安全短板。
在无保护或部分保护的 APK 中,攻击者可以展开 DEX 文件直接定位到生成的路由类和参数访问方法。通过构造恶意深链或使用反射调用这些未加固的方法,攻击者能够绕过应用主要的业务逻辑保护层。根据 Navigation type-safe routes 的文档,类型安全仅约束导航参数的形状和反序列化过程,并不承担登录校验、权限判定或业务规则。因此即使参数解析过程安全无误,只要路由入口未被 VMP 保护,后续的未加固代码依然可以被直接触发。
VMP 通常依赖于编译产物中的类列表和保护配置文件来确定处理范围。如果没有主动将 Navigation 生成类列入清单,加固工具很难自动推断这些隐式入口的可保护性。开发团队必须建立从 Gradle 任务输出目录提取生成类的机制,在每次构建后自动化生成保护清单,避免人工遗漏。下文列出检查路径,帮助团队系统性地识别和覆盖这些关键产物。
生成路由若直接连接到敏感页面,外部 Intent 或深链参数就可能抵达入口解析代码。比如用户 ID 由参数传入,而页面没有重新校验登录态与对象权限,构造后的参数可能触发不属于当前用户的数据查询。VMP 不能补上缺失的授权判断;检查时应先确认入口授权,再决定解析代码与业务函数各自的保护范围。
| 产物类型 | 典型类名 | 生成来源 | VMP 保护需覆盖 |
|---|---|---|---|
| Directions 类 | xxxFragmentDirections | Safe Args / Gradle 插件 | 是,路由跳转入口 |
| Args 参数类 | xxxFragmentArgs | Safe Args | 是,参数反序列化和传递 |
| 序列化器 companion | xxx$serializer | kotlinx.serialization | 是,编解码核心 |
| 导航字符串映射 | NavRoute 常量 | Navigation Compose | 视情况,可能被混淆需 keep |
- 是否明确区分手写的 ViewModel/Fragment 和 Navigation 自动生成的参数容器?
- 当前 VMP 保护配置是否仅扫描了业务包名,忽略了 build/generated 等生成类路径?
- 是否有自动化脚本从编译产物中提取所有 Navigation 生成类,并与保护清单交叉比对?
- 是否已确认生成类中包含的嵌套类型或枚举也被完整纳入保护范围?
Navigation 生成路由的产物分类与识别方法
要全覆盖生成路由,首先需要理解不同开发范式下的产物类型。如果项目使用 XML 导航图配合 Safe Args 插件,Gradle 会为每个目的地生成 Directions 类和提供参数访问的 Args 类,例如 UserProfileFragmentDirections 和 UserProfileFragmentArgs。这些类的包名、内部字段和静态方法均由 Safe Args 根据导航图 ID 和参数定义稳定生成,只要导航图或参数类型不变,产物就具有确定性,便于批量提取。
当采用 Navigation Compose 配合 Kotlin 序列化时,路由定义通常是带有 @Serializable 注解的数据类,导航调用通过类型安全的字符串路径完成。序列化编译器插件会为这些数据类生成 serializer() 方法,并在导航库内利用生成的序列化器完成参数编解码。此外,Kotlin/JS 或 Kotlin/MPP 场景下,序列化器可能存在于平台特定 source set 中,识别时需同时扫描 common 和 android 编译产物,确保无遗漏。
生成的参数类还可能包含嵌套类型或枚举。例如 Safe Args 会在 Args 类内部生成 Companion 对象或嵌套 Builder,Kotlin 序列化可生成 CompositeEncoder/Decoder 的内部类。在 VMP 保护时,主类与这些内部类都必须被完整包含,一旦某个内部类在 R8 阶段被内联或移除,即使主类存在也会导致运行时 NoClassDefFoundError 或序列化异常。识别时应当基于字节码层级的类完全限定名,而不仅仅是公开 API 可见的顶层类。
对于混合架构的项目,可能存在 XML Navigation 与 Compose Navigation 并存的情况。此时需要分别处理两种生成机制产生的产物。XML 导航主要产生基于 Bundle 的参数类,而 Compose 导航则更多依赖序列化数据类。保护清单必须合并这两类产物,并针对各自的序列化机制配置相应的 keep 规则,防止因机制差异导致部分路由在加固后失效。
| 导航范式 | 参数传递机制 | 生成类特征 | 主要风险点 |
|---|---|---|---|
| XML + Safe Args | Bundle 序列化 | Args/Directions 类 | 字段名混淆导致反射失败 |
| Compose + Serialization | kotlinx.serialization | Serializer 伴生对象 | 序列化器被 R8 裁剪 |
| 纯字符串路由 | 手动解析 Bundle | 无自动生成类 | 硬编码字符串被混淆 |
| 混合模式 | 多种机制并存 | 多类产物混合 | 保护清单不一致导致遗漏 |
- 是否已识别项目中所有使用的导航范式及其对应的生成机制?
- 是否检查了 Compose 路由中 @Serializable 数据类的伴生对象生成情况?
- 是否确认了嵌套类或内部类在保护清单中的完整性?
- 是否对混合架构项目的保护清单进行了合并去重处理?
路由参数类与序列化器的可达性验证
Navigation 参数传递依赖序列化机制,Safe Args 使用 Bundle 兼容的传统序列化,而 Compose 路由常用 kotlinx.serialization。在 R8/ProGuard 的压缩阶段,如果序列化器未被引用链命中,就会被裁剪掉。Kotlin 序列化文档指出,插件会生成描述符和字段序列化器,但编译器可能认为这些符号仅被反射使用,需要准确的 keep 规则保持其存在,否则 VMP 将无法处理不存在的类。
R8 keep rules best practices 文档提醒,反射、JNI 和间接入口应使用精确的类签名和成员匹配规则。对于 kotlinx.serialization,通常需要保留所有 @Serializable 类、它们的 $serializer 字段和 serializer() 方法。如果直接使用宽泛的 keepattributes 或保留整包,虽然短期可避开异常,但这种规则会掩盖真正的缺失,并且降低 R8 优化效果,间接扩大 VMP 需要处理的代码体量,增加加固失败概率。
在将生成产物送交 VMP 之前,要先确认目标类确实存在于最终 DEX。源码中的参数类可能被 R8 移除,也可能只出现在部分构建变体。本节 Bash 脚本调用 apkanalyzer 导出 APK 类表,再与审核后的保护清单逐行比对;缺少任一目标类时返回非零状态。该结果只证明类是否存在,不能证明它已被 VMP 正确处理。
验证过程中还需注意 ProGuard mapping 文件的影响。如果类被混淆,脚本比对时应使用混淆后的名称,或者在比对前先应用 mapping 文件进行转换。对于未混淆的类,直接比对完全限定名即可。无论哪种情况,验证的核心目标是确保 VMP 配置中引用的类在最终 APK 中真实存在且可被加载,避免因类缺失导致的运行时崩溃。
| 检查对象 | 预期存在形式 | 缺失后果 | 修复措施 |
|---|---|---|---|
| @Serializable 数据类 | APK 中包含对应 class | 无法实例化参数对象 | 添加 keep 规则 |
| $serializer 伴生对象 | APK 中包含 xxx$$serializer | 反序列化抛异常 | 保留序列化器类 |
| SerialDescriptor 字段 | 未被 R8 裁剪 | 序列化元数据丢失 | 精确 keep 成员 |
| 自定义序列化器 | 实现类存在于 APK | 自定义逻辑失效 | 纳入保护清单 |
- 是否已编写脚本自动验证保护清单类在 APK 中的存在性?
- 是否检查了 kotlinx.serialization 生成的 $serializer 类是否被保留?
- 是否确认了 R8 keep 规则足够精确以避免掩盖缺失问题?
- 是否在 CI 流程中集成了类存在性检查作为构建阻断条件?
#!/bin/bash
# 用法:check_classes.sh <apk> <class_list.txt>
# class_list.txt 每行一个完全限定类名
set -euo pipefail
APK="$1"
LIST="$2"
if [ ! -f "$APK" ] || [ ! -f "$LIST" ]; then
echo "Usage: $0 <apk> <class_list>"
exit 2
fi
# 提取 APK 中所有类(使用 apkanalyzer,需 Android SDK 25.3.0+)
if ! command -v apkanalyzer &> /dev/null; then
echo "Require apkanalyzer from Android SDK build-tools"
exit 3
fi
APK_CLASSES="$(mktemp)"
apkanalyzer dex list --proguard -f "$APK" > "$APK_CLASSES"
MISSING=0
while IFS= read -r TARGET_CLASS; do
# 跳过空行和注释
if [[ -z "$TARGET_CLASS" || "$TARGET_CLASS" =~ ^# ]]; then
continue
fi
if ! grep -qF "$TARGET_CLASS" "$APK_CLASSES"; then
echo "MISSING: $TARGET_CLASS"
MISSING=1
fi
done < "$LIST"
rm -f "$APK_CLASSES"
if [ "$MISSING" -ne 0 ]; then
echo "Some classes listed for protection are absent in APK"
exit 1
else
echo "All listed classes present"
exit 0
fi深链入口的信任边界与业务决策区分
Navigation 的 deep link 功能允许从外部 URL、通知或自定义 scheme 唤起应用并直接导航到某个目的地,参数经深链配置自动解析后注入目标页面。Navigation deep links 文档明确指出,路由匹配成功仅仅表示 URL 符合模式并完成了参数提取,不意味着参数值可信或请求来源合法。这意味着深链解析代码和目的地调用点必须独立处理来源校验,不能依赖系统保证,这是安全设计的基本原则。
从保护视角看,深链接口通常由 Manifest 的 intent-filter 与 Activity 或 Fragment 中的导航调用共同组成,而且不一定落在常规业务包名下。攻击者可能构造参数并传入入口解析逻辑;若页面只信任参数中的用户 ID,没有重新校验当前会话与对象权限,业务流程就可能被非法触发。应把输入校验和服务端授权作为独立控制,再评估入口解析与业务判断的 VMP 范围。
保护边界必须区分为两部分:其一是深链入口本身,包括解析 Intent 的导航器和生成的参数赋值代码;其二是参数被消费后的业务决策函数。VMP 处理时应当让这两部分都进入保护区域,而不是仅仅保护最终的业务函数。若因体积只保护核心业务,需在测试中验证深链入口仍受控,并留存例外记录。这种分层保护策略能有效防止攻击者利用入口层的弱点绕过核心逻辑。
在实际操作中,团队常误以为深链参数经过系统解析就是安全的。事实上,系统仅负责格式匹配和类型转换,不负责业务语义校验。攻击者可构造符合格式但语义恶意的参数,如越权的资源 ID 或特制的回调 URL。因此,必须在深链入口代码中加入严格的业务校验逻辑,并将该逻辑纳入 VMP 保护范围,确保校验过程不被篡改或绕过。
| 深链组件 | 攻击示例 | 是否需要 VMP 保护 | 保护后仍需做什么 |
|---|---|---|---|
| Manifest intent-filter | 任意外部应用发送匹配深链的 Intent | 无法保护 Manifest 本身 | 在代码入口进行来源校验 |
| Navigation 生成的 deepLink 方法 | 直接调用 deepLink 扩展,绕过 Activity 生命周期检查 | 需要,否则可被内部组件绕过 | 验证调用链是否保留原校准逻辑 |
| Args.fromBundle/fromSavedStateHandle | 构造恶意 Bundle 伪造参数 | 需要,参数解析函数不可信 | 在业务层弃用外部值前做完整性判断 |
| NavHost 的 findDestination | 查找未授权目的地后主动导航 | 如果路由表逻辑敏感,需保护 | review 导航图中的嵌套图权限 |
- 是否已识别所有注册了 intent-filter 的 Activity 及其对应的深链处理逻辑?
- 是否将深链参数解析代码纳入了 VMP 保护范围?
- 是否在深链入口处添加了来源校验和参数完整性验证逻辑?
- 是否记录了因体积限制未保护的深链入口及其风险控制措施?
VMP 保护前的清单核对与 keep 规则协同
将所有需要保护的生成类汇总成核对清单是降低遗漏风险的可靠方法。清单应至少包含三部分:Safe Args 相关的 Directions 和 Args 类、Kotlin 序列化路由的数据类和序列化器、以及扩展函数中直接操作导航控制器的入口方法。每项需标注对应的 Gradle 任务和输出路径,以便在构建流程中自动提取,例如将 navigation-args 生成目录下的 class 文件反编译为完全限定名,并去重加入清单,确保清单的准确性和完整性。
在 R8 优化阶段,如果使用了混淆,则生成类可能被重命名甚至内联,VMP 处理时如果依赖原始类名配置就会失败。因此 keep 规则必须与 VMP 配置同步:一方面要保留原始类型名称、字段签名用于序列化兼容,另一方面要让 VMP 能够识别混淆后的最终符号。实践中,可以先为敏感生成类设置 keep 规则防止混淆,然后在 VMP 配置中引用同样的类名,降低不匹配概率,提高加固成功率。
清单核对还需要考虑 variant 差异。有些路由参数只存在于特定 product flavor 或 build type,例如调试版本与发布版本的导航图可能含有不同的日志或调试路由。如果 VMP 配置是全局应用,可能在发布版本中因类不存在而报错或静默跳过。因此核对时应基于最终发布的 APK 提取类表,再次反向验证保护配置与真实产物的一致性,而不是仅依赖构建脚本的输出,避免环境差异导致的配置失效。
对于大型项目,建议建立自动化的清单生成和核对机制。通过解析 Gradle 构建输出和 APK 内容,动态生成保护清单并与现有配置比对。发现差异时自动报警或阻断构建,迫使开发人员及时更新配置。这种闭环管理能有效防止因人为疏忽导致的保护遗漏,确保每次发布都经过严格的安全检查。
| 检查项 | 比对内容 | 失败后果 | 检查方法 |
|---|---|---|---|
| Directions 类是否存在 | APK 类表 vs 导航图目的地数量 | 无法完成导航跳转,抛 ActivityNotFoundException | apkanalyzer + Gradle 任务名 |
| Args 类及其字段 | ProGuard mapping 中保留签名 | 参数传递为 null 或类型错误 | 列出构建产物字段与预期对比 |
| 序列化器 companion 对象 | APK 包含 xxx$$serializer | 反序列化抛异常,深链失效 | 脚本检查类存在性 |
| 深链分发方法 | 解析 DeepLink 注解生成方法 | 外部唤起走默认路由,绕过保护流程 | 导航图与反编译代码交叉比对 |
- 是否已建立自动化的保护清单生成和核对机制?
- 是否确认了 keep 规则与 VMP 配置的同步性?
- 是否针对不同构建变体验证了保护清单的适用性?
- 是否基于最终 APK 内容反向验证了保护配置的一致性?
构建后验证:用 instrumented test 确认兼容性
Android instrumented tests 文档强调了涉及真实运行时和框架 API 的行为必须通过设备端测试验证。VMP 保护往往会改变方法的执行方式或内存布局,即使所有类都存在且 keep 规则正确,运行时仍可能出现预期外的类加载顺序差异、序列化缓存未命中或是反射路径阻塞。对于 Navigation 而言,关键行为包括:安全参数传递后的目的地创建、深链匹配后的返回栈重建、以及 Safe Args 生成类的实例化,这些都需要在真实环境中验证。
验证策略应覆盖典型用户路径的端到端导航流程:冷启动深链触发 → 参数校验 → 业务函数调用 → 返回栈回退。instrumented test 可以使用 AndroidX Test 的 ActivityScenario 或 NavHostScenario 模拟完整导航,并在每个断点断言页面状态与参数值。如果保护后测试不通过,则需回溯到类清单是否完整、混淆映射是否被破坏、或者 keep 规则是否保留了必要的 Protobuf/序列化支持类,快速定位问题根源。
单一设备或单一 Android 版本的通过不能代表整个设备矩阵。由于不同厂商实现的 ART 行为差异以及 VMP 对不同 API level 的适配策略不同,应当在至少两种 API level 和不同芯片架构的设备上执行该套 instrumentation 测试。对于导航组件,尤其要验证 Bundle 序列化在高低版本间的行为是否一致,以及带有自定义 Parcelable 的参数在加固后是否可以正常传递。测试报告应记录通过条件,作为后续版本升级时的对照基线。
测试用例的设计应重点关注边界条件和异常场景。例如,传入非法类型的参数、构造超长的深链 URL、或在低内存环境下触发导航等。这些场景更容易暴露出 VMP 保护带来的兼容性问题。通过全面的测试覆盖,可以提前发现并修复潜在隐患,确保加固后的应用在各种环境下都能稳定运行。
| 测试维度 | 覆盖场景 | 预期结果 | 失败处理 |
|---|---|---|---|
| API Level 兼容性 | 最低支持版本至最新稳定版 | 导航流程正常,参数传递正确 | 排查 ART 行为差异 |
| 芯片架构兼容性 | ARMv7, ARM64, x86_64 | 无崩溃,性能无明显下降 | 检查 native 层适配 |
| 深链场景覆盖 | 冷启动,热启动,后台唤起 | 返回栈重建正确,参数解析无误 | 修正深链处理逻辑 |
| 异常参数测试 | 空值,类型错误,超长字符串 | graceful 降级或友好提示 | 增强参数校验逻辑 |
- 是否编写了覆盖主要深链和导航路径的 instrumented test?
- 是否在至少 2 种 API level 设备上通过关键导航场景测试?
- 测试是否包含自定义参数类型在 VMP 处理后的序列化/反序列化?
- 失败的用例是否追溯到缺失的生成类或丢失的 keep 规则?
- 测试结果是否作为基线文档化,便于回归?
常见失败条件:即使保护了仍可能出错的场景
保护了生成路由但运行时仍然崩溃,首要嫌疑是 R8 删除了序列化相关的间接依赖。kotlinx.serialization 的核心运行时类型 SerialDescriptor 可能仅被字节码内的字符串字面量引用,普通压缩规则容易将其移除。如果项目使用了自定义序列化器,这些序列化器本身也需要被 R8 保留并被 VMP 保护。仅保护顶层数据类而遗漏序列化器模块,会导致运行时抛出 SerializationException,造成应用崩溃。
第二个常见原因是混淆与 Safe Args 不兼容。Safe Args 生成固定键名的 Bundle 存取代码,若字段被混淆而键名未同步,导航时可能抛异常。开发团队有时为减少体积,对 Args 类启用了混淆,但在 VMP 处理后又因类结构变化导致字段名不可读。此时必须在 keep 规则中为参数类排除混淆,或者确保 VMP 前的混淆一致性建立后不轻易变更,避免运行时反射失败。
多模块工程中,路由参数类可能定义在 domain 或 data 模块中,而 Navigation 生成代码在 app 模块。如果 VMP 仅处理 app 模块产物,其他模块的参数类就保持未保护状态,攻击者可以直接分析这些模块的 JAR 并获取参数结构。即使 app 层加固完善,通过数据模块泄漏的参数格式也可以用来构造恶意输入。所以保护清单必须跨模块收集,以最终 APK 全部 class 集合为基准,而不是以模块保护配置为单位。
此外,某些第三方库可能与 VMP 存在兼容性问题。例如,某些库依赖特定的类加载顺序或反射机制,VMP 的虚拟化执行可能干扰这些机制。在引入新库或升级现有库时,应重新评估其对 VMP 保护的影响,并进行充分的测试验证。及时发现并解决兼容性问题,可以避免上线后出现严重故障。
| 症状 | 可能的直接原因 | 验证方法 | 修复方向 |
|---|---|---|---|
| 深链唤起 crash | 深链分发方法未纳入保护或被 R8 删除 | 检查 mapping 中深链方法是否存在 | 添加 keep 规则并加入保护清单 |
| 参数反序列化异常 | 序列化器缺失或 SerialDescriptor 被裁剪 | 反编译检查 $serializer 类存在性 | 精确保留所有序列化器 |
| Safe Args 导航失败 | Args 字段被混淆导致反射失败 | 查看 ProGuard 输出确认 Args 包名规则 | 保持 Args 类及其字段未混淆 |
| 多模块参数解析错误 | 依赖模块参数类未被 VMP 覆盖 | 检查 APK 中对应类是否在保护配置内 | 跨模块扫描生成类并集中纳入保护 |
- 是否检查了 SerialDescriptor 等间接依赖是否被 R8 保留?
- 是否确认了 Args 类及其字段未被混淆?
- 是否跨模块收集了所有参数类并纳入保护清单?
- 是否评估了第三方库与 VMP 的兼容性?
安全行动一览:从检查到保护的全流程决策表
综合前述各阶段,项目应将 Navigation 路由的 VMP 保护纳入构建流水线的固定环节,而不是一次性的手工操作。行动流程从生成产物提取开始,经过可达性验证、清单配置、R8 协同、instrumented test 验证,直至最终的发布签收。每一步都有明确的通过和阻断条件,确保流程可复现、可审计。这种标准化的流程能有效降低人为错误,提高安全防护的可靠性。
本节表格按产物提取、可达性验证、R8 协同和设备测试列出核对项。类表缺项时先修正构建或清单;mapping 与预期不一致时调整 keep 规则;设备用例失败时缩小保护范围并重新构建。每一步都要保存输入产物与检查结果,不能只记录一个通过状态。
在执行这一流程时,团队还需要形成一份“保护覆盖声明”,注明本次发布保护的生成路由范围、深链入口列表、以及已知的局限性(如某些 flavor 的路由未纳入),供安全审计使用。没有这份声明,后续版本可能因为导航图变更而不知不觉失去保护。文档化的声明有助于追踪安全状态,明确责任边界。
每次修改 Navigation 图、升级 Navigation 或 Kotlin 序列化插件、增加构建变体后,都要重新导出生成类清单,核对 mapping 与 VMP 配置,并执行深链、返回栈和参数解析用例。旧清单只能作为差异基线,不能直接沿用为新产物的验收结论。
| 流程阶段 | 判断依据 | 通过条件 | 阻断动作 |
|---|---|---|---|
| 产物提取 | 基于 Gradle 任务输出的 class 列表 | 生成类全限定名列表非空且与导航图条目数一致 | 否则阻断,检查 Gradle 任务配置 |
| 可达性验证 | APK 类表与清单比对脚本 | 所有保护清单类均在 APK 中,脚本返回 0 | 否阻断,修复 keep 规则后重新构建 |
| R8 协同 | 混淆映射文件与 keep 规则 | 生成参数类未被混淆或字段签名保留 | 发现有混淆则禁止发布,调整规则 |
| instrumented 测试 | 关键导航和深链用例 | 指定设备矩阵全部通过 | 不通过则否定该版本保护有效性 |
- 是否将 VMP 保护纳入了构建流水线的固定环节?
- 是否明确了各阶段的通过和阻断条件?
- 是否形成了“保护覆盖声明”文档供安全审计使用?
- 是否建立了定期回顾和更新保护策略的机制?
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 类型安全的 Navigation 路由只保证参数形状匹配,不承担登录、授权或业务规则。 | Navigation type-safe routes | 该文档仅定义编译期参数类型安全,不证明运行时参数可信或业务逻辑安全。 |
| 深链匹配成功不代表参数可信,路由目的地仍需独立验证来源和权限。 | Navigation deep links | 文档不提供如何进行业务授权校验的实现指导,只说明系统行为的约束。 |
| Safe Args 为 XML 导航图生成类型化参数访问代码,生成规则稳定但需按最终构建产物验证。 | Navigation Safe Args | 只表明生成代码的兼容性依赖于 Gradle 插件和导航库版本一致,不意味着生成代码天然可在 VMP 保护后直接运行。 |
| kotlinx.serialization 生成的序列化器在 R8 阶段可能被裁剪,需要精确 keep 规则。 | Kotlin serialization | 文档描述标准用法,不证明第三方程序列化库或自定义序列化器具有同样的保持特性。 |
| 反射入口和间接调用的代码必须使用精确的 keep 规则,宽泛规则会掩盖边界错误。 | R8 keep rules best practices | keep 规则只影响 R8 留用集,不确定这些类在 VMP 环境下的表现。 |
| 涉及 Android 运行时和系统 API 的测试应使用 instrumented test 验证,单元测试无法替代。 | Android instrumented tests | 单一设备通过测试不能推广到所有 API 等级和芯片架构。 |
| Safe Args 的参数访问依赖于字段名反射,混淆后可能出错。 | 工程判断 | 此推断来自对生成代码的通用观察,未对特定版本 Safe Args 进行逆向确认。 |
| 多模块工程中参数类可能分布在非 app 模块,只保护 app 模块会导致攻击面残留。 | 工程判断 | 具体风险程度取决于参数类的敏感性和模块划分,项目证据尚未接入数据。 |
工程常见问题
Safe Args 生成的 Directions 类要不要加 VMP?
理想情况下,所有向外部暴露或参与参数传递的生成类都应纳入保护。如果因体积限制不得不部分保护,至少要覆盖深链入口、序列化器和包含敏感字段的 Args 类,并记录排除项以供审计。Directions 类作为路由跳转入口,通常建议纳入保护。
如何自动提取 Safe Args 生成类用于保护清单?
可以通过 Gradle 任务在 build/generated/source/navigation-args 目录收集 .class 或 .java 文件,利用反射或反汇编工具转换为完全限定名,然后与 CI 流程结合输出文本文件。但最终核对仍要以 APK 内容为准,确保清单准确性。
保护深链入口后是否就不需要业务层校验了?
仍需要。系统深链分发只能保证路由匹配,无法判断调用来源是否合法或参数是否被篡改。所有关键业务决策都必须在受保护区域内重新验证参数完整性,不能依赖入口完成所有安全检查。深链保护与业务校验互为补充。
VMP 保护后 Safe Args 代码是否会因混淆而失效?
有可能。如果对参数类启用了混淆且未保留字段名,Safe Args 的反射可能失败。推荐的做法是 keep 住参数类的字段名或完全禁止混淆这类生成代码,并在 instrumented test 中验证。混淆策略需谨慎制定。
instrumented test 通过是否足以证明保护的正确性?
不能完全证明。instrumented test 可以验证典型用例在测试设备上的功能正确性,但无法覆盖所有厂商定制和 API 组合。需要配合代码审查和静态分析,同时扩大设备矩阵。测试通过仅是必要条件。
如果后续 Navigation 图变更,旧的保护配置是否自动适用?
不会自动适用。新增的目的地和参数会生成新类,需要重新运行生成类提取脚本,更新保护清单并验证。依赖人工同步,建议通过 CI 强校验跳过新类则阻断构建。配置更新需与代码变更同步。