先看结论与判断条件
- 发布判定对象是最终交付 APK 的二进制 Manifest,源码模板和 merged report 只用于解释来源。
- 精确允许值表必须绑定 applicationId、release variant、渠道和环境,不能从当前候选自动学习基线。
- 除了 ${...} 残留,还要识别已被替换但来自 staging、test 或其他 flavor 的合法字符串。
- authority、host、scheme、process 与 meta-data 各自使用集合、类型、模板和环境规则,未知键进入人工评审。
- 回执绑定 APK SHA-256、签名证书、package、versionCode、策略版本与固定解码工具,候选变化即失效。
- 静态 Manifest 通过不代表运行授权正确,仍需验签、安装、深链、provider 与服务端业务校验回归。
直接答案:检查最终二进制 Manifest,并用变体允许值而非字符串黑名单判定
发现 Manifest 占位值问题不能只搜索源码中的 ${...}。发布门禁应先冻结待签名或已签名 APK,解码最终 AndroidManifest.xml,再把 authority、host、scheme、process、provider 标识和业务 meta-data 与该 release 变体的允许值表逐项比较。未解析模板、测试环境值、其他 flavor 值和未知键都应阻断。
占位符可能在合并时正确替换,却替换成错误环境的合法字符串;也可能被依赖库、product flavor 或命令行覆盖。单纯搜索美元符号只能发现明显残留,无法识别 api-staging.example 与 production 候选不匹配。门禁需要同时持有候选哈希、variant 身份、合并来源和明确的期望值。
静态 Manifest 审计只能证明清单声明与策略一致,不能证明组件运行时授权、服务端配置或深链业务校验正确。修复后必须从可信输入重建、重新验签并覆盖安装与入口回归。没有真实候选和项目允许值时,不应报告某个 App 已通过或某种风险已被阻断。
| 对象 | 关键属性 | 串用后果 | 默认门禁 |
|---|---|---|---|
| ContentProvider | authorities | 冲突或错误共享 | 必须唯一 |
| App Link | scheme/host | 打开错误环境 | 精确允许值 |
| 组件 | process | 隔离与生命周期变化 | 白名单匹配 |
| FileProvider | authorities | URI 授权失效 | 绑定 applicationId |
| meta-data | name/value | SDK 或业务配置漂移 | 已知键和值 |
| network config | resource | 证书或域策略变化 | 资源存在且对应 |
源码模板不是发布事实,最终 APK 才是判定对象
Android Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据,但源码只是一层输入。主模块、build type、flavor、依赖 AAR、manifestPlaceholders 和构建插件会共同生成最终二进制 Manifest。门禁必须读取交付候选,不能用仓库文件替代最终观察。
加固或重打包可能再次处理 Manifest、注入组件或替换 application 属性。即使 Gradle merged manifest 报告正确,后处理候选仍可能带入旧值。因此要保存三个阶段的摘要:合并后、加固前候选和最终签名候选,并把差分限定在已审批字段;出现未解释变化时停止发布。
二进制 Manifest 应使用固定版本的受信工具解码,并记录工具摘要和原始 APK 摘要。只读取 strings 输出会丢失属性类型、命名空间和组件归属,容易产生误报。报告至少保存 package、version、组件名、属性路径、解码值和来源阶段,且不把秘密值写入公开日志。
| 证据 | 能确认 | 不能确认 | 用途 |
|---|---|---|---|
| 源码 Manifest | 模板意图 | 最终合并值 | 代码评审 |
| merged report | Gradle 合并来源 | 后处理结果 | 追溯覆盖 |
| 未签名 APK | 构建输出 | 发布身份 | 加固前差分 |
| 签名 APK | 交付清单与摘要 | 运行时授权 | 发布门禁 |
| 设备 dump | 安装可见状态 | 所有业务路径 | 入口回归 |
| 服务端配置 | 环境目标 | 客户端声明正确 | 端到端核对 |
允许值表必须绑定 applicationId、variant、渠道和环境
build type、product flavor、source set、applicationId 与签名配置会组合成不同变体。允许值表不能只有 production 一列,而应使用可复核键,例如 applicationId + variantName + distributionChannel + environment。每个键列出精确 authority、host、scheme、process、允许的 meta-data 以及禁止模式。
允许值不宜从当前 APK 自动学习,否则错误候选会成为自己的基线。策略应来自受审配置仓库或发布审批,包含版本、负责人、变更原因和生效日期。新增 host、provider 或 SDK 元数据必须先修改策略并评审,再允许候选通过,不能在流水线临时追加白名单。
测试和预发值常常不是敏感信息,但暴露它们可能引导错误流量或泄露内部拓扑。公开文章只使用 example 域名;生产门禁日志可存哈希、键名和环境标签,把真实值保存在最小权限制品中。任何长期 API Key 或服务密钥都不应作为普通 Manifest meta-data。
| 字段 | 示例语义 | 比较方式 | 变更审批 |
|---|---|---|---|
| applicationId | 应用身份 | 完全相等 | 发布负责人 |
| variantName | release 组合 | 完全相等 | 构建负责人 |
| authority | provider 身份 | 集合相等 | 组件负责人 |
| deepLinkHost | 生产主机 | 允许集合 | 业务与安全 |
| process | 进程布局 | 路径精确匹配 | 架构负责人 |
| metaData | 公开配置 | 键和值规则 | SDK 负责人 |
Manifest 合并来源用于定位根因,不能替代成品检查
当最终值错误时,merged manifest report 与 blame 信息可以追踪是主模块、flavor、build type、依赖 AAR 还是 tools:replace 覆盖。直接和传递依赖的解析图还能指出哪个 artifact 引入组件或 meta-data。定位时必须使用同一 release 变体,debug 依赖树不具有替代性。
tools:node、tools:replace 和占位符覆盖会改变合并优先级。修复应回到最小源头:删除不应出现的依赖声明、限定 flavor、改正 manifestPlaceholders 或增加明确的合并规则。不要在最终 APK 上手工替换字符串,也不要添加宽泛 tools:replace 掩盖多个来源冲突。
依赖图只能缩小范围,不能证明某个 SDK 是唯一根因;一个值可能由根项目属性、CI 参数或后处理脚本覆盖。门禁记录每个异常字段的最终值、期望规则、首次出现阶段和可追溯来源,来源未知时保持 blocked,而不是根据包名猜测责任方。
authority、host、scheme、process 与 meta-data 分别验证
authority 通常参与 ContentProvider 和 FileProvider 的系统级寻址,应在整个已安装包中保持预期唯一。门禁既检查字面值,也检查是否仍含占位符标记、是否以当前 applicationId 为前缀、是否与已知系统或其他渠道值冲突。多个 provider 可以有不同规则,不能用一个 startsWith 放过全部。
深链 scheme 和 host 决定外部 URI 进入哪个组件。production 候选应拒绝 staging、test、localhost、保留示例域以及未审批的通配 host;同时检查 autoVerify、path 规则和导出状态。静态匹配正确后,仍要在设备上用允许与拒绝 URI 验证实际解析和业务内二次校验。
android:process 会改变组件所在进程、初始化顺序、内存与 IPC 边界;meta-data 可能决定 SDK App ID、环境、provider 或功能开关。进程名比较应支持以冒号表示的私有进程和完整进程名,但只能匹配策略列出的形式。未知 meta-data 键应进入人工评审,不能默认忽略。
| 属性 | 精确检查 | 典型拒绝 | 后续验证 |
|---|---|---|---|
| authorities | 集合和模板规则 | 重复或环境错配 | provider 访问 |
| scheme | 大小写与允许集合 | 测试协议 | URI 解析 |
| host | 规范化后精确值 | staging/未知域 | App Link |
| process | 组件路径和值 | 未知远端进程 | 启动与 IPC |
| meta-data name | 已知键集合 | 新增未审批键 | SDK 初始化 |
| meta-data value | 类型与环境规则 | 模板残留/错环境 | 运行配置 |
签名身份与候选摘要要和 Manifest 回执绑定
应用签名文档区分应用签名密钥、上传密钥、证书和 Play App Signing 职责。Manifest 门禁如果只保存解码文本而不绑定 APK SHA-256 与签名证书摘要,就可能把一个包的通过结果误用于另一个包。每份回执应包含候选摘要、证书摘要、package、versionCode 和策略版本。
修复占位值会改变 APK 字节并使原签名失效,必须通过正式构建和签名流程重新产生候选。不能对已签名 APK 解包、替换字符串后重新打包,再沿用原测试报告。若使用 AAB,还要对分发系统生成的 APK 进行代表性核对,明确本地与线上交付证据的边界。
签名正确只确认发布身份与完整性链的一部分,不证明 authority、host 或业务 meta-data 正确。相反,Manifest 值正确也不能替代证书连续性。发布报告将两项分成独立 gate,任一失败都阻断,并保留可追溯的命令、工具版本和退出状态。
用外部策略检查解码后的 Manifest 摘要
下面的 Python 脚本不解析二进制 APK,也不接触签名密钥。上游使用固定工具把最终 Manifest 提取为公开 JSON 摘要,策略文件给出 applicationId、variant、允许的 authority、host、scheme、process 与 meta-data 键。脚本验证两个输入结构与 SHA-256,再逐项比较并非零退出。
规则同时拒绝 ${name}、@placeholder/name、双花括号和常见 test 或 staging 标记,但这些模式只是补充;真正的环境串用由精确允许值识别。示例只输出字段路径和错误类型,不打印真实值。生产系统可以把异常值做受控哈希,并在权限受限的回执中保存明细。
脚本通过只证明提供的摘要符合提供的策略。它不证明摘要来自完整最终 APK、策略审批正确、签名连续或设备入口正常。流水线必须把输入文件摘要与候选绑定,固定提取工具,随后执行验签、安装和代表性深链与 provider 回归。
import hashlib
import json
import re
import sys
from pathlib import Path
if len(sys.argv) != 3:
raise SystemExit("usage: gate_manifest.py manifest-summary.json policy.json")
manifest_path = Path(sys.argv[1]).resolve()
policy_path = Path(sys.argv[2]).resolve()
if not manifest_path.is_file() or not policy_path.is_file():
raise SystemExit("manifest summary or policy file is missing")
def load_object(path):
with path.open("r", encoding="utf-8") as stream:
value = json.load(stream)
if not isinstance(value, dict):
raise SystemExit(f"{path.name} root must be an object")
return value
manifest = load_object(manifest_path)
policy = load_object(policy_path)
required = {"applicationId", "variant", "authorities", "hosts", "schemes", "processes", "metaData"}
if set(policy) != required:
raise SystemExit("policy keys do not match the reviewed schema")
if manifest.get("applicationId") != policy["applicationId"]:
raise SystemExit("applicationId differs from policy")
if manifest.get("variant") != policy["variant"]:
raise SystemExit("variant differs from policy")
forbidden_marker = re.compile(r"(\$\{[^}]+\}|\{\{[^}]+\}\}|@[a-z]+/|staging|localhost|\.test\b)", re.I)
errors = []
for field in ("authorities", "hosts", "schemes", "processes"):
actual = manifest.get(field)
allowed = policy[field]
if not isinstance(actual, list) or not isinstance(allowed, list):
errors.append(f"{field}: expected arrays")
continue
if sorted(set(actual)) != sorted(set(allowed)):
errors.append(f"{field}: set differs")
if any(not isinstance(item, str) or forbidden_marker.search(item) for item in actual):
errors.append(f"{field}: forbidden_marker or forbidden environment marker")
actual_meta = manifest.get("metaData")
allowed_meta = policy["metaData"]
if not isinstance(actual_meta, dict) or not isinstance(allowed_meta, dict):
errors.append("metaData: expected objects")
else:
if set(actual_meta) != set(allowed_meta):
errors.append("metaData: key set differs")
for key, expected in allowed_meta.items():
actual = actual_meta.get(key)
if actual != expected:
errors.append(f"metaData.{key}: value differs")
if isinstance(actual, str) and forbidden_marker.search(actual):
errors.append(f"metaData.{key}: forbidden marker")
if errors:
print(json.dumps({"status": "failed", "errors": sorted(set(errors))}, indent=2))
raise SystemExit("Manifest policy gate rejected the supplied candidate summary")
for path in (manifest_path, policy_path):
digest = hashlib.sha256(path.read_bytes()).hexdigest()
print(f"{path.name} sha256={digest}")
print(json.dumps({"status": "passed", "next": "bind receipt to APK and signing certificate"}, indent=2))运行回归验证声明是否真正对应目标入口
静态通过后,在正式 applicationId 的干净设备上安装候选,核对 package manager 可见的 provider authority、组件进程和 intent filter。分别发送允许、错误环境、未知 host 和越界 path 的测试 URI,确认只有允许入口到达,并且业务层仍验证登录、对象归属和动作权限。
涉及 provider 时覆盖 URI grant、读写权限、宿主销毁和多进程初始化;涉及 SDK meta-data 时核对启动日志的公开环境枚举,不输出 App Secret。测试应记录 API、ABI、厂商、安装来源、候选哈希与预期结果,单一设备通过不能外推完整兼容矩阵。
运行时发现错环境不能通过服务端临时兼容来掩盖,因为客户端仍会把用户导向错误资源。应撤回候选、修正构建输入并重建。若服务端必须短时阻断错误入口,也要作为事故缓解单独记录,不能把缓解措施写成 Manifest 已修复。
把检查放进发布流水线,并保留可回滚的证据
流水线顺序建议为:解析 variant 身份、构建候选、保存摘要、解码最终 Manifest、运行允许值门禁、比较加固前后差分、验签、设备回归,再进入发布审批。任何一步改变候选字节,都必须使后续回执失效并重新执行,避免结果错绑。
NIST SSDF 强调来源、构建、验证与变更证据,OWASP MASVS-RESILIENCE 把抗篡改和抗逆向定位为纵深防御。两者都不提供项目自动通过结论。工程上应保存不可变回执、策略版本、异常处置和回滚目标,同时把 Manifest 门禁与服务端授权、安全发布链分开。
关于最终清单合并的完整差分可继续阅读本站文章 /zh-cn/articles/final-apk-manifest-merge-drift/;需要评审发布候选的 APK 加固与清单边界,可通过御盾中央平台 https://www.leonadev.com/console/ 提交脱敏的候选身份、变体策略和回归要求。
- 冻结 APK、签名证书、variant 与策略版本摘要。
- 用固定工具解码最终二进制 Manifest。
- 精确比较 authority、host、scheme、process 与 meta-data。
- 追溯 merged report、依赖图和后处理阶段差分。
- 修复后从可信输入重建并重新签名。
- 在设备上覆盖允许与拒绝入口。
- 回执只声明检查范围,不外推运行时或攻击结论。
- 内链与御盾中央行动入口保持可访问。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Manifest 是 Android 组件、权限、intent filter、SDK 约束和应用元数据的声明入口。 | Android app manifest 说明清单的核心声明职责,支持在最终候选中审计这些属性。 | 静态清单不能证明运行时访问控制、业务校验或服务端授权没有被放宽。 |
| 允许值策略需要绑定实际构建变体。 | Android build variants 说明 build type、product flavor、source set、applicationId 和签名配置会组合成不同输出。 | 同一仓库或同名 release 不代表所有渠道、环境和证书使用相同 Manifest 值。 |
| 依赖合并来源应使用同一 release 变体的解析图定位。 | Android Gradle dependency resolution 说明直接与传递依赖的解析和版本选择,支持追踪引入 Manifest 声明的 artifact。 | 依赖树只能缩小排查范围,不能证明某个 SDK、CI 参数或后处理步骤是唯一根因。 |
| Manifest 回执必须与应用签名身份和实际候选绑定。 | Sign your Android app 区分应用签名密钥、上传密钥、证书与 Play App Signing 责任,支持独立记录证书证据。 | 官方文档不能确认某个实际 APK 使用正确生产证书,也不证明其 Manifest 值正确。 |
| 安全发布过程应保留来源、构建、验证和变更证据。 | NIST SP 800-218 SSDF 把供应链风险及开发、验证与发布证据纳入组织实践。 | SSDF 不定义具体 Android 占位符规则,也不提供御盾产品或项目的自动通过结论。 |
| Manifest 门禁与 VMP 都不能替代服务端授权和完整发布链。 | OWASP MASVS-RESILIENCE 将抗篡改与抗逆向定位为移动端纵深防御控制。 | 控制目录不证明任何候选达到特定防护强度,运行和授权仍需项目证据。 |
| 仅搜索 ${...} 无法发现已替换为错误环境值的占位符串用。 | 工程判断:语法残留检测与变体允许值比较回答不同问题,必须同时运行才能覆盖显式和隐式错配。 | 禁止模式只是补充,精确允许值必须由项目负责人审批,不能凭文章示例生成。 |
| Manifest 异常修复后必须从可信输入重建,而不是修改已签名 APK。 | 工程判断:修改 ZIP 内容会改变候选字节并使原签名与测试证据失效,重建才能保留来源和审批链。 | 具体密钥托管、AAB 分发和上线审批由项目流程决定,正文不包含任何凭据。 |
| 未知 meta-data 应阻断或人工确认,不能默认忽略。 | 工程判断:依赖升级或注入步骤可能新增初始化配置,未审字段会改变 SDK、环境或功能行为。 | 允许完全忽略的键必须在版本化策略中明确列出,并记录负责人和失效条件。 |
| 本文没有提供具体项目通过、兼容率、排名、收录或攻击阻断结论。 | 项目证据尚未接入:缺少目标 APK、正式策略、签名证书与设备回执。 | 正文只提供可执行的提取、比较、归因和发布检查方法。 |
工程常见问题
为什么源码 Manifest 中没有 ${...} 仍可能串用环境?
占位符可能已被成功替换,但替换值来自错误 flavor、CI 参数或依赖覆盖。必须把最终值与当前 release 变体的允许值表比较。
merged manifest 报告通过后还要检查 APK 吗?
要。加固、重打包或其他后处理可能再次修改 Manifest;merged report 用于定位合并来源,最终 APK 才是交付事实。
只用 strings 命令搜索 APK 是否足够?
不足。字符串输出缺少属性类型、命名空间和组件路径。应使用固定工具解码二进制 Manifest,并保存字段级摘要与工具版本。
authority 应怎样建立允许规则?
按具体 provider 建立精确值或受控模板,绑定 applicationId 与 variant,同时检查集合唯一性。不要用一个宽泛前缀放过全部 provider。
能否让流水线从当前 APK 自动生成允许值?
不能。错误候选会把自身变成基线。允许值应来自受审策略仓库,包含版本、负责人、原因和生效范围。
修复占位值后可以保留原签名和测试回执吗?
不可以。任何 APK 字节变化都会改变候选身份并使原回执失效,应通过正式流程重建、签名、扫描和设备回归。
Manifest 静态检查通过是否表示深链安全?
不表示。还要在设备上验证 URI 解析、导出和权限,并由业务层检查登录、对象归属、参数与服务端授权。
脚本中的 test、staging 黑名单能代替允许值吗?
不能。它只能捕捉常见标记,无法覆盖自定义环境名或看似合法的错误域名;精确允许值才是主要判据。