先看结论与判断条件
- Remote Config 参数会在客户端被采用和观察,不能存放秘密,也不能独自授予高价值操作权限。
- 默认值、已获取值、已激活值和当前生效值是不同状态,门禁必须覆盖首次启动、离线、失败与旧缓存。
- 条件优先级决定最终参数,版本、地区、用户属性和随机百分位条件需要在变体与测试矩阵中明确。
- VMP 候选应是稳定、敏感且可独立回归的本地决策核心,不是 fetch、activate、序列化或 SDK 回调。
- 远端参数可以收紧本地能力或提供风险输入,但服务端授权的对象、主体和动作不能下放给客户端布尔值。
- rollout 与紧急停用都有传播延迟和离线边界,回滚参数不能恢复已损坏状态或替代候选回滚。
先把配置分发与安全裁决分开
Remote Config 解决的是参数分发与条件选择,不是建立可信执行环境。Android 客户端最终要取得、缓存、激活并读取参数,运行中的值能够被观察,相关调用路径也位于用户控制的设备上。因此它适合控制功能呈现、实验参数、风险阈值和紧急收敛,不适合保存密钥,也不适合用一个 allowPurchase 或 isAdmin 参数直接授予敏感操作。
VMP 的作用也要放在正确层级。它可以提高本地决策代码被直接阅读和修改的成本,适合保护风险评分、状态组合、离线限制和业务关键分支;它不能让远端参数变成秘密,不能保证设备一定取得最新值,也不能替代服务端按用户、对象和动作执行授权。若服务器只相信客户端返回的“远端配置允许”,整个授权链仍由公共客户端控制。
架构评审应把链路拆成参数定义、默认值、fetch、activate、条件解析、本地决策、敏感动作和服务端裁决。每一步记录输入、失败状态和责任人,再选择保护边界。SDK 获取与缓存属于易变的平台胶水,通常保留原状;稳定的本地业务决策可以纳入 VMP;高价值权限和数据访问必须由可信服务端再次确认。
| 环节 | 主要责任 | 是否适合 VMP | 不能承担 |
|---|---|---|---|
| 默认值 | 定义首次启动和失败行为 | 随决策一起评审 | 证明远端已同步 |
| fetch | 获取并缓存远端参数 | 通常排除 | 使新值立即生效 |
| activate | 切换当前生效参数集 | 通常排除 | 替代业务回滚 |
| 条件选择 | 确定目标参数值 | 记录输入与顺序 | 证明客户端已拉取 |
| 本地决策 | 组合状态、阈值和限制 | 敏感核心可保护 | 完成服务端授权 |
| 服务端裁决 | 校验主体、对象和动作 | 不在客户端 VMP 范围 | 依赖客户端布尔值 |
默认值决定首次启动和故障下限
Firebase Remote Config for Android 要求应用提供应用内默认值,并按 fetch 与 activate 生命周期采用远端参数。默认值不是开发占位,它会在首次安装、尚未获取、获取失败或参数缺失时直接影响行为。安全评审应逐项回答:默认值是否放宽敏感能力,旧客户端是否理解新参数,类型转换失败时走哪条路径,以及没有已激活值时是否能保持可接受的最小功能。
安全关键参数应优先采用失败关闭或服务端确认,而不是默认开放。例如远端参数可以决定是否显示某个入口,但真正提交高价值操作时,服务器仍需验证权限;远端参数也可以降低本地风险阈值,却不应在网络失败时把阈值恢复到无限制。默认值的目标不是让页面永远可用,而是在不确定状态下保持已声明的风险边界。
默认值还要按构建变体核对。开发变体可能默认打开诊断入口,生产变体必须具有不同的资源、applicationId 或参数文件。Android build variants 说明 build type、product flavor 和 source set 会组合成不同变体,同一仓库不代表默认值相同。门禁应读取最终候选中的默认资源,并把结果绑定到具体 variant 与产物摘要。
| 状态 | 可用值来源 | 建议行为 | 禁止假设 |
|---|---|---|---|
| 首次启动未 fetch | 应用内默认值 | 执行安全默认路径 | 远端值马上可用 |
| fetch 成功未 activate | 旧激活值或默认值 | 保持当前状态并记录待激活 | 获取即等于生效 |
| activate 成功 | 新激活参数集 | 重算受影响决策 | 所有业务状态自动恢复 |
| fetch 失败 | 旧激活值或默认值 | 按陈旧期限与失败策略处理 | 失败时默认开放 |
| activate 失败 | 当前激活值 | 保持旧状态并报告异常 | 部分字段安全切换 |
| 参数缺失或类型错误 | 显式回退值 | 失败关闭或请求服务端 | SDK 会自动纠正语义 |
fetch、activate 和 current value 不能混成一个状态
fetch 负责取得并缓存候选参数,activate 负责让缓存参数成为当前值。若监控只记录 fetch success,就可能把尚未激活的值当作生效配置;若只记录最终布尔值,又无法判断它来自应用默认值、旧缓存还是新远端值。关键决策应记录参数 key、值来源、fetch 状态、activate 状态、模板或配置版本以及观察时间,但日志不应包含令牌和个人数据。
陈旧缓存需要明确策略。对纯展示参数,继续使用上次激活值通常可接受;对风险阈值,要设置可辩护的有效期并在过期后收紧;对依赖服务端授权的动作,客户端缓存只影响界面或预检查,服务器仍按当前规则拒绝未授权请求。用一个通用 keep-last-value 处理所有参数,会把产品连续性偏好误用到安全边界。
激活通常会同时影响一组参数,业务代码却可能在不同时间读取它们。若决策需要多个 key 保持一致,应在本地创建不可变快照并验证 schema 版本,再一次性送入决策函数。不要在一次判断中多次从 SDK 查询可变值,否则 activate 恰好发生时可能得到混合版本。VMP 可以保护快照后的决策逻辑,但不能修复上游读取的非原子性。
条件顺序本身就是决策输入
Firebase Remote Config conditions 说明参数可以按应用版本、平台、国家地区、用户属性和随机百分位等条件选择值,条件顺序会影响最终结果。团队不能只审查默认值和某个命中值,还要保存条件列表、优先级与兜底值。两个条件同时匹配时谁先决定结果,必须在测试用例中可复现,不能依赖控制台截图和操作者记忆。
条件中的用户属性和地区并不等于授权身份。它们可用于目标群体、体验差异和风险输入,但不能证明当前调用者有权读取某个对象或执行交易。特别是客户端能够观察最终值时,攻击者可以了解被分配的策略,并尝试改变本地决策路径。服务端必须重新验证真正的授权条件,客户端条件只能用于提示、预筛选或纵深限制。
随机百分位适合灰度,但测试需要固定代表桶。团队应覆盖默认条件、每个高优先级条件、多个条件同时匹配、无条件匹配以及参数缺失。应用版本条件还要覆盖升级前后,因为旧客户端可能不认识新 schema。测试证据应绑定配置版本、应用候选和输入属性,不能用一台设备的一次命中概括全部人群。
| 条件 | 适合用途 | 必须验证 | 不能替代 |
|---|---|---|---|
| 应用版本 | 兼容开关和迁移节奏 | 旧版、新版与未知版 | 候选身份校验 |
| 平台 | Android 与其他端差异 | 平台默认与缺失字段 | 服务端权限 |
| 国家地区 | 合规与功能可见性 | 边界地区和离线状态 | 用户真实身份 |
| 用户属性 | 已定义群体体验 | 属性缺失、陈旧和变更 | 对象级授权 |
| 随机百分位 | 灰度和实验分桶 | 代表桶与稳定性 | 实际安装覆盖 |
| 条件优先级 | 解决多条件命中 | 顺序变化与兜底 | 自动正确合并 |
按决策类型选择 VMP 保护对象
第一类是展示与体验参数,例如页面布局、文案变体和非敏感功能入口。它们通常不值得进入 VMP,保持代码简单更利于升级和诊断。第二类是风险输入,例如阈值、设备状态权重和离线次数上限。若本地组合算法包含商业敏感规则,可把稳定的计算核心纳入保护,同时保持参数读取、类型转换和错误报告在外层。
第三类是收敛参数,例如 disableFeature、requireOnlineCheck 或 tightenLimit。远端配置可以让客户端走更保守路径,但必须考虑离线和缓存延迟;服务器侧仍应能独立拒绝危险操作。第四类是授权结果,例如 allowTransfer、isEntitled 或 canReadObject。这类布尔值不应由 Remote Config 直接承担,即使读取后的 if 分支进入 VMP,也只是把错误授权隐藏得更深。
选择 VMP 候选时看四个条件:逻辑是否敏感、是否稳定、是否可以用确定性输入回归、保护后失败是否可诊断。SDK 回调、Task 链、序列化 DTO、资源 ID、生成代码和 fetch/activate 生命周期都容易随 SDK 变化,应排除。适合保护的是纯函数式决策核心,它接收规范化快照和业务状态,返回允许、拒绝或要求服务端确认,并带可观测原因码。
| 决策类型 | 参数作用 | VMP 建议 | 最终安全边界 |
|---|---|---|---|
| 展示配置 | 改变界面与文案 | 通常排除 | 产品功能测试 |
| 非敏感实验 | 选择体验分支 | 通常排除 | 实验与回滚治理 |
| 本地风险输入 | 调整阈值或权重 | 保护稳定计算核心 | 服务端复核高价值动作 |
| 紧急收敛 | 禁用或限制本地能力 | 保护收敛决策可选 | 服务端独立拒绝 |
| 离线限制 | 控制次数、期限和状态 | 可保护离线状态机 | 联网后服务端对账 |
| 业务授权 | 决定主体与对象权限 | 不得只保护客户端分支 | 服务端强制裁决 |
rollout 和 kill switch 都有传播边界
Firebase Remote Config rollouts 支持向部分目标用户逐步开放参数并依据监控调整或回滚。它适合降低变更半径,但不保证每个客户端立刻 fetch 与 activate。离线设备、受节流影响的请求、长期后台进程和未触发激活的会话都可能继续使用旧值。因此紧急停用不能只依赖客户端参数,关键服务应有服务端拒绝路径。
rollout 回滚只改变后续采用的配置值,不能恢复已经修改的本地数据库、已提交的业务动作或损坏的产物。如果新参数触发了不可逆迁移,回退参数可能让旧代码路径面对新数据格式。发布设计应先定义状态兼容与恢复动作,再决定参数能否灰度。控制台上的回滚按钮不是应用事务回滚,也不是 APK 版本召回。
Firebase Remote Config use cases 展示按版本或用户群控制功能暴露等场景,但安全关键默认值与失败路径仍需预先存在于应用。用例可帮助选择配置方式,不能证明某个项目的紧急停用时效、加固兼容或用户覆盖。发布前应在真实候选上模拟未 fetch、fetch 未 activate、旧缓存、目标条件和回滚后重启等路径。
用策略检查器约束默认值和决策映射
下面的 Python 脚本读取一份公开安全的策略映射 JSON,检查生命周期失败策略、参数 key 唯一性、默认值、条件优先级、本地决策函数和服务端强制字段。每个参数标记 decisionClass、failurePolicy、conditions、decisionFunction、vmpScope 与 serverEnforced。脚本不连接 Firebase,不读取真实用户属性,也不输出配置值,适合在代码审查前发现设计责任混乱。
授权类参数必须使用布尔 false 默认值,必须 serverEnforced,并采用 deny 或 require-server 失败策略;风险输入如果进入 VMP,必须映射到非 SDK 胶水的本地决策函数。条件 priority 要唯一且连续,避免审计清单与控制面顺序不一致。顶层生命周期策略固定表达 fetch 失败保留上次激活值、首次无值使用默认值、activate 失败保留当前值。项目可扩展这些规则,但不得静默接受未知枚举。
脚本通过只证明策略文件满足当前静态约束。它不验证 Firebase 控制台真实模板,不证明客户端已取得参数,也不证明 VMP 转换后的候选运行正确。实际门禁还要把策略摘要与候选摘要、Remote Config 模板版本和测试回执绑定,并对每个决策函数执行固定输入与失败路径回归。
from pathlib import Path
import json
import re
import sys
if len(sys.argv) != 2:
raise SystemExit(2)
policy_path = Path(sys.argv[1])
if not policy_path.is_file():
raise SystemExit(2)
try:
policy = json.loads(policy_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
raise SystemExit(2)
required_lifecycle = {
"fetchFailure": "keep-last-activated",
"noActivatedValue": "use-defaults",
"activateFailure": "keep-current"
}
if policy.get("lifecycle") != required_lifecycle:
raise SystemExit(3)
parameters = policy.get("parameters")
if not isinstance(parameters, list) or not parameters:
raise SystemExit(2)
allowed_classes = {"presentation", "risk-input", "authorization", "disable-only"}
allowed_failures = {"use-default", "deny", "require-server"}
allowed_scopes = {"protect", "exclude"}
key_pattern = re.compile(r"^[a-z][a-zA-Z0-9_]{2,63}$")
glue_pattern = re.compile(r"(?i)(fetch|activate|remoteConfig|firebase|callback|listener)")
issues = []
seen_keys = set()
for item in parameters:
if not isinstance(item, dict):
raise SystemExit(2)
key = item.get("key")
if not isinstance(key, str) or not key_pattern.fullmatch(key) or key in seen_keys:
issues.append({"key": str(key), "issue": "invalid-or-duplicate-key"})
continue
seen_keys.add(key)
decision_class = item.get("decisionClass")
failure_policy = item.get("failurePolicy")
vmp_scope = item.get("vmpScope")
function_name = item.get("decisionFunction")
if decision_class not in allowed_classes or failure_policy not in allowed_failures or vmp_scope not in allowed_scopes:
issues.append({"key": key, "issue": "unknown-policy-enum"})
if not isinstance(function_name, str) or not function_name.strip():
issues.append({"key": key, "issue": "missing-decision-function"})
if vmp_scope == "protect" and glue_pattern.search(function_name or ""):
issues.append({"key": key, "issue": "sdk-glue-selected-for-vmp"})
conditions = item.get("conditions", [])
priorities = [condition.get("priority") for condition in conditions if isinstance(condition, dict)]
if len(priorities) != len(conditions) or priorities != list(range(len(priorities))):
issues.append({"key": key, "issue": "condition-priority-gap"})
if decision_class == "authorization":
if item.get("default") is not False:
issues.append({"key": key, "issue": "authorization-default-not-deny"})
if item.get("serverEnforced") is not True:
issues.append({"key": key, "issue": "authorization-not-server-enforced"})
if failure_policy not in {"deny", "require-server"}:
issues.append({"key": key, "issue": "unsafe-authorization-fallback"})
report = {"variant": policy.get("variant"), "parameterCount": len(parameters), "issues": issues}
print(json.dumps(report, ensure_ascii=False, indent=2))
if issues:
raise SystemExit(3)回归矩阵必须覆盖配置与候选组合
静态清单通过后,测试应按参数来源和生命周期展开。至少覆盖应用默认值、旧激活值、新 fetch 未 activate、新激活值、fetch 失败、activate 失败、类型错误和条件冲突。每个场景固定配置版本、应用候选、构建变体、用户属性和预期原因码。只测 happy path 会遗漏首次安装、离线和陈旧缓存,而这些恰好是安全默认值真正生效的地方。
VMP 保护前后应使用同一输入快照比较决策结果,重点观察布尔值、阈值边界、状态迁移、异常语义和线程行为。测试通过要绑定同一候选摘要,不能用未保护构建替代。若保护后的决策函数无法诊断,应保留外层原因码和安全日志,但不要记录远端用户属性、认证令牌或完整业务对象。可观测性服务于定位,不应扩大数据暴露。
变体矩阵也不能省略。release、地区和渠道变体可能带不同默认资源、applicationId、依赖和签名配置。一个 variant 通过不代表其他 variant 可以继承结论。若同一 Remote Config 项目服务多个变体,应验证条件能准确区分目标应用,并为错误项目、错误包名和未知版本设置安全兜底。
| 场景 | 输入状态 | 关键断言 | 失败后决定 |
|---|---|---|---|
| 首次启动 | 只有应用默认值 | 敏感动作不被默认放宽 | 修正默认值后重建 |
| fetch 未 activate | 新缓存加旧当前值 | 继续使用一致旧快照 | 修正状态观测 |
| fetch 失败 | 旧缓存或无缓存 | 执行对应陈旧与失败策略 | 阻断开放型回退 |
| activate 失败 | 当前值保持不变 | 不产生混合参数集 | 修正读取快照 |
| 多条件命中 | 固定版本、地区和属性 | 按登记优先级选择 | 校正条件顺序 |
| 服务端拒绝 | 客户端配置显示允许 | 高价值动作仍被服务端拒绝 | 阻断客户端单边授权 |
发布材料要明确边界与下一动作
一份可执行的评审材料应包含最终候选摘要、variant、默认值清单、Remote Config 模板版本、条件顺序、参数分类、失败策略、本地决策函数、VMP 方法清单和服务端裁决接口。每项参数说明它可以放宽还是只能收紧、离线时使用什么值、何时要求服务端,以及哪个测试回执覆盖它。这样审查者能沿责任链判断,而不是只看到一串 key。
OWASP MASVS-RESILIENCE 将移动端抗逆向与抗篡改视为纵深防御控制。这个定位适用于本场景:VMP 可以提高本地风险决策被修改的成本,但不能改变客户端是公共环境的事实,也不证明任何候选达到特定防护强度。没有同候选的变换、构建和运行回执时,应记录待验证,不写性能数字、阻断结论或客户效果。
准备接入评估时,可在御盾中央平台提交候选、默认值与模板摘要、条件表、失败策略、服务端授权边界、决策函数清单和回归矩阵。登录、注册、申请、价格与控制台动作统一由中央平台承接。评审重点是先把授权留在可信侧,再选择值得保护且能够验证的本地核心,而不是把所有 Remote Config 调用一起纳入 VMP。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 客户端需要提供应用内默认值,并按 fetch 与 activate 生命周期采用 Remote Config 参数。 | Firebase Remote Config for Android 描述默认值、获取、激活和客户端读取流程。 | Remote Config 不是秘密存储或强制授权,客户端最终能观察其采用的参数。 |
| Remote Config 条件可以使用应用版本、平台、国家地区、用户属性和随机百分位等输入。 | Firebase Remote Config conditions 描述可用条件、参数值选择与条件优先级。 | 条件命中不证明客户端已经拉取或激活新值,也不能替代服务端拒绝危险操作。 |
| Remote Config rollout 可以向部分目标用户逐步开放参数并根据监控调整或回滚。 | Firebase Remote Config rollouts 描述 rollout 的目标、监控和参数回滚机制。 | rollout 不保证离线客户端立即收到新值,也不能恢复已经损坏的本地状态。 |
| Remote Config 可用于按版本或用户群控制功能暴露等场景。 | Firebase Remote Config use cases 描述版本控制、用户群和功能发布相关用法。 | 用例不证明安全关键默认值、紧急停用时效、加固兼容或当前项目效果。 |
| build type、product flavor 和 source set 会组合成不同 Android 构建变体。 | Android build variants 描述变体、applicationId、资源与签名配置的组合。 | 同一仓库不能证明所有 variant 具有相同默认值、证书和运行行为。 |
| 移动端抗逆向与抗篡改属于纵深防御控制。 | OWASP MASVS-RESILIENCE 给出移动应用韧性控制范围。 | 控制目录不证明具体候选达到某种保护强度,也不替代服务端授权和完整发布链。 |
| Remote Config 授权类参数必须失败关闭并由服务端最终强制执行。 | 工程判断:客户端参数、缓存和分支都位于公共设备,不能成为高价值操作的唯一可信根。 | 具体服务端授权模型需要根据主体、对象、动作和业务数据另行设计与验证。 |
| VMP 应优先保护规范化参数快照后的稳定本地决策核心。 | 工程判断:把 fetch、activate、SDK 回调和易变胶水纳入会扩大兼容与诊断成本,却不改变授权边界。 | 是否适合保护仍需结合真实候选、方法清单、回归证据和失败语义评估。 |
工程常见问题
把 Remote Config 的授权判断做 VMP 后能否替代服务端权限检查?
不能。VMP 只能提高本地分支被分析和修改的成本,Remote Config 参数仍由公共客户端采用。高价值操作必须由服务端按主体、对象和动作重新授权。
fetch 成功是否表示新参数已经生效?
不表示。fetch 通常只取得并缓存参数,activate 才切换当前值。监控和测试要分别记录 fetch、activate、值来源与配置版本。
紧急 kill switch 为什么不能只放在 Remote Config?
离线、缓存、节流和未激活状态都会产生传播延迟。客户端参数可以收敛能力,但关键服务还应能独立拒绝危险动作,并准备状态恢复方案。
哪些 Remote Config 相关方法适合进入 VMP?
优先考虑接收规范化参数快照和业务状态的稳定决策函数。SDK fetch、activate、回调、序列化、资源读取和生成胶水通常排除,以控制兼容与诊断成本。
一个 release 变体测试通过后能否覆盖其他渠道?
不能直接继承。build type、flavor、source set、applicationId、资源、依赖和签名配置可能不同,每个交付 variant 都要核对最终默认值与真实候选。
申请 Remote Config 与 VMP 边界评估需要什么材料?
准备最终候选、variant、默认值与模板摘要、条件优先级、失败策略、服务端授权边界、决策函数清单、VMP 方法清单和回归矩阵,再通过御盾中央平台提交。