先看结论与判断条件

  • 发布判定对象是最终交付 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 已通过或某种风险已被阻断。

最容易发生占位值串用的 Manifest 表面
对象关键属性串用后果默认门禁
ContentProviderauthorities冲突或错误共享必须唯一
App Linkscheme/host打开错误环境精确允许值
组件process隔离与生命周期变化白名单匹配
FileProviderauthoritiesURI 授权失效绑定 applicationId
meta-dataname/valueSDK 或业务配置漂移已知键和值
network configresource证书或域策略变化资源存在且对应

源码模板不是发布事实,最终 APK 才是判定对象

Android Manifest 定义组件、权限、intent filter、SDK 约束和应用元数据,但源码只是一层输入。主模块、build type、flavor、依赖 AAR、manifestPlaceholders 和构建插件会共同生成最终二进制 Manifest。门禁必须读取交付候选,不能用仓库文件替代最终观察。

加固或重打包可能再次处理 Manifest、注入组件或替换 application 属性。即使 Gradle merged manifest 报告正确,后处理候选仍可能带入旧值。因此要保存三个阶段的摘要:合并后、加固前候选和最终签名候选,并把差分限定在已审批字段;出现未解释变化时停止发布。

二进制 Manifest 应使用固定版本的受信工具解码,并记录工具摘要和原始 APK 摘要。只读取 strings 输出会丢失属性类型、命名空间和组件归属,容易产生误报。报告至少保存 package、version、组件名、属性路径、解码值和来源阶段,且不把秘密值写入公开日志。

证据层级与能回答的问题
证据能确认不能确认用途
源码 Manifest模板意图最终合并值代码评审
merged reportGradle 合并来源后处理结果追溯覆盖
未签名 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应用身份完全相等发布负责人
variantNamerelease 组合完全相等构建负责人
authorityprovider 身份集合相等组件负责人
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 回归。

Manifest 占位值与环境允许值门禁
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 黑名单能代替允许值吗?

不能。它只能捕捉常见标记,无法覆盖自定义环境名或看似合法的错误域名;精确允许值才是主要判据。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: APK、DEX 与签名完整性如何一起验收