先看结论与判断条件
- 模型文本不是可信控制指令,系统提示词可以约束回答方向,却不能替代应用侧的确定性安全门禁。
- URL 校验必须基于标准解析后的结构化字段,而不是前缀匹配、字符串包含或对模型说明的信任。
- App Links 验证解决域名与应用签名的关联,不会自动完成登录状态、对象归属、金额、角色和一次性授权校验。
- 路由匹配成功只说明地址能映射到页面,冷启动、返回栈、异常参数和业务动作仍要独立验收。
- 导出组件与 PendingIntent 会扩大调用和重放边界,不能因为入口来自应用内部的 AI 页面就跳过组件与目标限制。
- 最安全的执行模型是生成、解析、决策、确认、授权和提交分层,每层只接收上一层的结构化结果。
模型输出只能进入建议通道
当聊天助手返回“打开订单”“进入会员页”或一段 Deep Link 时,模型完成的是文本生成,不是业务授权。输出可能来自不完整上下文、错误理解、外部内容影响或格式漂移。即使地址看起来像应用自己的域名,也可能指向不存在的路由、错误对象或超出当前用户权限的动作。产品应把这段输出标记为 proposal,后续只允许确定性代码读取,不能直接调用系统打开地址。
Firebase AI Logic system instructions 说明系统指令会随请求影响模型行为,同时应用仍需控制输入、输出和工具权限。这个边界很关键:提示词可以要求模型只生成允许域名,却不能保证每次输出都符合要求,也不能处理用户会话、对象归属和一次性业务状态。把提示词当安全边界,会让策略跟随自然语言而漂移,审计时也无法回答哪条代码拒绝了越权地址。
建议通道应保留原始模型文本、模型请求标识、解析结果和拒绝原因,但日志必须避免敏感查询参数。只有解析器输出结构化 action candidate 后,策略引擎才继续判断。若模型返回多个地址、混入说明文字、使用未知 scheme 或无法唯一解析,默认动作应是展示普通文本或要求用户重新选择,而不是猜测最可能的目标。
| 阶段 | 输入 | 允许的输出 | 失败时处理 |
|---|---|---|---|
| 模型生成 | 会话与可公开上下文 | 文本或候选动作 | 保留为文本,不执行 |
| URL 解析 | 单个候选字符串 | 结构化 scheme、host、path、query | 拒绝歧义或非法格式 |
| 路由策略 | 规范化 URL | 允许的内部动作类型 | 拒绝未知域名和路由 |
| 用户确认 | 动作摘要与影响 | 明确同意或取消 | 取消后不提交 |
| 业务授权 | 当前会话与对象 | 一次可执行命令 | 返回无权或状态已变化 |
先做结构化解析,再谈白名单
白名单不能用 startsWith 或字符串包含实现。URL 可能包含大小写差异、编码字符、用户信息、端口、片段、重复查询参数和多次解码后的歧义。可靠流程应先用平台 URL 解析器得到 scheme、host、port、path、query 和 fragment,再按统一规则规范化。解析失败、出现用户名密码段、使用非预期端口或包含 fragment 时,可以直接拒绝,避免后续组件各自解释出不同目标。
scheme 需要按用途分层。面向外部来源的可验证链接优先使用 https 和受控域名;应用自定义 scheme 若确有历史兼容需求,应限制精确值、精确路由和调用来源,不能把任意 host 当作内部命令。对于模型生成的地址,未知 scheme、file、content、javascript 或其他未登记协议都不应进入 Intent。拒绝规则要在创建 Intent 之前运行,避免解析副作用或错误组件匹配。
host 白名单也必须做精确比较。允许 example.com 不等于允许 example.com.attacker.invalid,允许某个子域也不代表允许所有后缀。国际化域名、尾点和默认端口要经过统一规范化后再比较。path 应按解码后的段匹配固定模板,不能让编码斜杠或路径穿越改变路由结构。query 参数则按路由独立声明名称、数量、类型、长度和可选性。
| 字段 | 允许方式 | 需要拒绝 | 拒绝原因 |
|---|---|---|---|
| scheme | 精确允许 https 或登记协议 | 未知及可访问本地内容的协议 | 协议决定调用面 |
| host | 规范化后精确命中 | 后缀伪装、空 host 与用户信息 | 字符串相似不代表同一域名 |
| port | 缺省或策略登记端口 | 未登记端口 | 避免指向不同服务入口 |
| path | 解码后按段匹配模板 | 空段、编码分隔符和未知层级 | 防止路由解释分歧 |
| query | 路由声明的键与类型 | 重复键、未知键和超长值 | 减少参数走私和歧义 |
| fragment | 通常不参与动作 | 任何未定义片段 | 避免组件解释不一致 |
App Links 验证的是关联,不是业务许可
Android App Links overview 说明 App Links 通过 Manifest URL、Digital Asset Links 和应用签名证书建立域名与应用的关联。这能减少受验证 https 链接被其他应用随意接管的问题,也让系统知道哪些链接应交给当前应用。它解决的是系统级关联与路由归属,不会判断链接中的订单是否属于当前用户,也不会验证当前会话是否允许付款、删除或分享。
因此域名验证通过后,应用仍要执行对象级授权。一个指向订单详情的合法链接可能引用别人的订单,一个指向退款页面的合法链接可能超出当前角色,一个优惠领取链接也可能已经过期。服务端应根据当前登录主体、对象状态、操作权限和一次性条件重新决策,客户端路由白名单只负责把请求送到正确的安全检查入口。
Verify Android App Links 提供重置、重新验证和读取各域名验证状态的方法,适合在发布前确认设备上的关联结果。这个状态仍不能替代端到端回归。测试必须覆盖未安装后首次打开、冷启动、后台恢复、已登录和未登录、验证失败、浏览器回退及异常参数。模型生成链接的验收还要增加多地址、说明文字和未知路由输入。
| 问题 | App Links 能回答 | 应用或服务端继续回答 | 证据 |
|---|---|---|---|
| 域名是否关联应用 | 通过关联文件和签名证书验证 | 确认发布配置属于当前候选 | 域名验证状态 |
| 链接映射哪个页面 | 系统选择匹配的应用入口 | Navigation 解析具体路由 | 路由匹配回执 |
| 参数是否可信 | 不能 | 类型、范围、对象存在性校验 | 参数门禁结果 |
| 用户是否有权 | 不能 | 会话、角色和对象归属授权 | 服务端授权回执 |
| 动作是否仍有效 | 不能 | 状态、时效与幂等检查 | 提交时最新状态 |
路由匹配和动作执行必须拆开
Navigation deep links 说明深链会匹配路由参数,并影响冷启动与任务返回栈。路由器的职责是把结构化地址映射到页面或导航目标,不应同时完成高风险业务操作。即使链接写着 confirm=true,也只能决定页面展示确认状态,不能在页面创建时自动提交支付、删除、授权或外发数据。导航是用户界面入口,业务命令必须经过另一套明确接口。
路由白名单要面向动作语义设计。只读详情、筛选列表和帮助页面可以在基础校验后导航;会改变服务器状态或暴露敏感信息的路由,应先生成安全预览,展示对象、影响和来源,再要求用户确认;涉及角色、资金、设备绑定或授权变更的动作,必须在提交时重新验证会话并由服务端裁决。模型不能通过 query 参数降低确认等级。
返回栈也是安全体验的一部分。冷启动深链可能绕过用户通常经过的页面,返回键也可能暴露不应出现的上层页面。测试应记录入口、登录中转、确认页、目标页和返回路径。若应用需要先登录,登录完成后只能恢复经过清洗的 action candidate,不应重新执行原始模型字符串,避免会话切换期间地址内容发生未审计变化。
导出组件决定谁能触达入口
Android exported component risk 指出导出组件和 intent filter 会扩大跨应用调用面,需要显式权限和输入校验。一个 Activity 因 Deep Link 而导出后,外部应用也可能直接构造 Intent 到达入口,来源不再只包括浏览器和应用内 AI 页面。因此安全逻辑必须放在被调用组件或其统一策略层,不能只放在聊天界面的点击回调里。
组件清单应记录 exported 值、intent filter、所需权限、允许 action、category、data 约束以及实际业务处理器。能不导出的组件保持不导出,外部入口只负责解析和转发到内部不可导出页面。解析结果应经过不可变数据结构传递,内部页面不要再次从原始 Intent 读取未经校验的 extras、data 或 clipData。
来源判断不能只依赖 referrer、调用包名或界面路径,因为这些信号可能缺失,且不等于业务授权。更可靠的原则是无论谁调用,都执行相同的 URL、路由、参数、会话和对象校验。若某动作只允许应用内部触发,应使用不可导出组件或受权限保护的显式入口,而不是在导出组件里检查一个容易遗漏的布尔参数。
| 清单项 | 安全问题 | 可执行决策 | 验证方式 |
|---|---|---|---|
| exported | 外部应用能否直接触达 | 仅保留必要的外部入口 | 检查 Manifest 合并结果 |
| intent filter | 哪些 action 和 data 会匹配 | 收窄 scheme、host 与 path | 构造允许和拒绝用例 |
| 组件权限 | 调用方是否需要平台权限 | 敏感入口增加适当权限 | 未授权调用应失败 |
| 原始 Intent | 未校验字段会否继续传播 | 入口处转换为清洗对象 | 内部页面拒绝原始 extras |
| 业务处理器 | 导航是否直接触发状态变更 | 拆出确认与授权层 | 冷启动和恢复流程回归 |
PendingIntent 不能成为绕过通道
PendingIntent security risks 说明可变性、目标组件和一次性标志会影响重放与重定向风险。若应用把模型生成的 Deep Link 包装进 PendingIntent,用于通知、快捷操作或后台回调,原有 URL 门禁仍然必须先执行。PendingIntent 不是可信封装,它只是让另一方在未来以创建者身份触发预先描述的操作。
创建时应优先使用明确目标组件和不可变配置,并按业务需求考虑一次性使用。传入的数据应是已经解析和规范化的 action id,而不是原始 URL。若动作需要最新授权,PendingIntent 触发后仍要读取当前会话和服务端状态,不能沿用创建通知时的权限结论。通知存在时间越长,订单、角色、设备和风险状态越可能变化。
正确 flags 也不能保证返回栈与业务恢复正确。通知点击、AI 页面点击和外部 App Link 可能进入不同任务状态,测试要覆盖应用未运行、后台存活、进程被回收和用户切换账号等场景。任何路径都只能抵达确认或详情入口,不能因为来自 PendingIntent 就自动执行高风险命令。
用策略文件把路由与参数固定下来
实现层可以把允许的 host、path 模板、参数类型、确认等级和所需权限放进版本化策略文件。模型只输出候选 URL,校验器把它转换成 action candidate。策略命中后仍不执行,只返回 route、规范化参数、requiresConfirmation 和 requiredPermission,交给后续页面与业务服务。这样既能审计允许面,也能在模型格式变化时保持确定性。
下面的 Python 代码演示只读校验。它拒绝非 https、用户信息、端口、fragment、未知 host、未知 path、重复参数、未知参数和类型错误,并从策略中读取路由要求。代码不发起网络请求,不创建 Intent,也不执行任何动作。真实 Android 实现应使用平台 URI API和严格的路由对象,服务端授权仍在提交时完成。
错误必须是可观察的失败,而不是悄悄降级到第一个页面。日志可以记录 policy version、route id 和拒绝码,但应对对象 id 和查询值做最小化处理。模型输出包含无法解析内容时,界面可以显示“无法打开该操作”并保留普通文本。不要把失败地址交给通用浏览器作为自动回退,否则未知目标仍可能离开应用安全边界。
from pathlib import Path
from urllib.parse import parse_qs, unquote, urlsplit
import json
import re
import sys
if len(sys.argv) != 3:
raise SystemExit(2)
policy_path = Path(sys.argv[1])
candidate = sys.argv[2]
if not policy_path.is_file() or len(candidate) > 2048:
raise SystemExit(2)
policy = json.loads(policy_path.read_text(encoding="utf-8"))
try:
parsed = urlsplit(candidate)
port = parsed.port
except ValueError:
raise SystemExit(3)
if parsed.scheme.lower() != "https":
raise SystemExit(3)
if parsed.username or parsed.password or port or parsed.fragment:
raise SystemExit(3)
host = (parsed.hostname or "").rstrip(".").lower()
if host not in set(policy.get("allowedHosts", [])):
raise SystemExit(3)
path = unquote(parsed.path)
if "//" in path or ".." in path:
raise SystemExit(3)
route = next((item for item in policy.get("routes", []) if item.get("path") == path), None)
if route is None:
raise SystemExit(3)
query = parse_qs(parsed.query, keep_blank_values=True)
if any(len(values) != 1 for values in query.values()):
raise SystemExit(3)
parameter_rules = route.get("parameters", {})
if set(query) - set(parameter_rules):
raise SystemExit(3)
normalized = {}
for name, rule in parameter_rules.items():
values = query.get(name)
if rule.get("required") and not values:
raise SystemExit(3)
if not values:
continue
value = values[0]
kind = rule.get("type")
if kind == "uuid" and not re.fullmatch(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", value):
raise SystemExit(3)
if kind == "enum" and value not in set(rule.get("values", [])):
raise SystemExit(3)
if kind == "integer":
if not value.isdecimal():
raise SystemExit(3)
number = int(value)
minimum = rule.get("minimum")
maximum = rule.get("maximum")
if not isinstance(minimum, int) or not isinstance(maximum, int) or minimum > maximum:
raise SystemExit(3)
if number < minimum or number > maximum:
raise SystemExit(3)
value = number
normalized[name] = value
result = {"routeId": route.get("id"), "parameters": normalized, "requiresConfirmation": bool(route.get("requiresConfirmation")), "requiredPermission": route.get("requiredPermission")}
if not result["routeId"] or not result["requiredPermission"]:
raise SystemExit(3)
print(json.dumps(result, ensure_ascii=False, sort_keys=True))确认、授权和提交各自解决不同问题
用户确认解决的是意图可见性。确认页应使用应用根据结构化参数重新查询的数据展示对象名称、动作影响和可撤销性,不能直接展示模型生成的描述并把它当事实。用户点确认后,客户端生成一次提交请求,但服务端仍要重新验证会话、角色、对象归属、对象状态和幂等键。确认不能赋予用户本来没有的权限。
授权解决的是当前主体是否可以在当前状态下执行动作。Deep Link 中的 userId、role、price、permission 或 confirmed 参数都不应成为授权依据。对象标识可以帮助定位资源,真正的角色和权限来自服务端会话与策略。若用户在确认期间切换账号、对象状态变化或动作已执行,提交应失败并返回新的可理解状态。
幂等和时效解决的是重复执行。模型可能重复给出同一链接,用户也可能多次点击,PendingIntent 和恢复流程还会增加重放机会。状态变更命令应使用由应用或服务端生成的短期操作标识,并在服务端实现幂等。URL 本身不应携带长期有效的高权限凭据;若业务需要一次性令牌,也要限制受众、动作、对象、时效和使用次数。
| 门禁 | 核心问题 | 不能替代 | 失败结果 |
|---|---|---|---|
| URL 解析 | 地址是否唯一且结构合法 | 域名关联和业务授权 | 拒绝候选 |
| 路由白名单 | 应用是否声明该动作 | 参数真实性和对象权限 | 不创建导航动作 |
| 用户确认 | 用户是否理解并同意 | 服务端权限 | 取消且不提交 |
| 会话授权 | 当前主体是否有权 | 用户确认和幂等 | 返回无权 |
| 对象状态 | 动作在当前时刻是否有效 | 链接格式 | 返回状态已变化 |
| 幂等控制 | 重复请求会否重复生效 | 权限检查 | 复用已有结果或拒绝 |
发布前用拒绝用例证明边界
验收不能只测几条正确链接。测试集应包含未知 scheme、后缀伪装域名、用户信息、非预期端口、编码路径分隔符、重复参数、未知参数、类型错误、未登录、无权对象、过期对象、多次点击和进程恢复。每条用例绑定应用候选、策略版本、路由版本和预期拒绝层,才能判断失败是否发生在正确位置。
平台回执也要分开保存。App Links 的域名验证状态证明系统关联,Navigation 测试证明路由和返回栈,组件测试证明外部调用面,业务接口回执证明对象授权与幂等。任何单项通过都不能扩大成整体安全结论。没有真实候选和服务端记录时,只能说明设计方法和待验证范围,不能声称某应用已阻断风险。
准备移动 AI 操作安全评估时,可整理模型可能生成的动作类型、允许域名、路由策略、参数 schema、导出组件清单、确认等级、服务端权限和拒绝用例,再通过御盾中央平台提交需求。评审重点应放在模型建议如何被收窄为可审计命令,而不是继续堆叠提示词。模型输出保持不可信,应用才能在模型升级后继续守住同一业务边界。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 系统指令会影响模型行为,但应用仍需控制输入、输出和工具权限。 | Firebase AI Logic system instructions 说明系统指令随请求影响生成行为及应用侧控制责任。 | 系统指令不能完全阻止越权、泄露或格式漂移,不能作为 Deep Link 执行边界。 |
| Navigation 深链会匹配路由参数,并影响冷启动与任务返回栈。 | Navigation deep links 描述深链声明、参数匹配和导航行为。 | 路由匹配成功不证明参数可信,也不代表目标业务动作已经授权。 |
| Android App Links 通过域名声明、关联文件和应用签名证书建立应用关联。 | Android App Links overview 描述 Manifest URL、Digital Asset Links 与签名证书的关联。 | 域名关联不校验登录会话、对象归属、操作权限或动作时效。 |
| 设备可以重置、重新验证并读取各域名的 App Links 验证状态。 | Verify Android App Links 给出验证状态检查和复核方法。 | 系统验证状态不能替代冷启动、返回栈、异常参数和业务授权回归。 |
| 导出组件与 intent filter 会扩大跨应用调用面。 | Android exported component risk 说明 exported 配置、intent filter 与外部调用风险。 | exported 值只是入口条件,不能单独证明组件输入和业务处理安全。 |
| PendingIntent 的可变性、目标组件和一次性设置会影响重放与重定向边界。 | PendingIntent security risks 说明相关配置对安全行为的影响。 | 正确 flags 不保证 Deep Link 参数、返回栈、会话授权和状态恢复正确。 |
| 模型生成地址应先转换为结构化 action candidate,再进入用户确认和业务授权。 | 工程判断:文本生成、URL 解析、导航和状态变更属于不同信任层,合并执行会让拒绝责任不可审计。 | 分层设计需要在真实应用候选和服务端上验证,本文不提供项目通过结论。 |
| Deep Link 中的角色、价格、权限和确认参数不能作为服务端授权依据。 | 工程判断:客户端地址由外部或模型提供,当前会话、对象归属和状态必须由可信业务系统重新决定。 | 具体授权模型取决于业务,本文不声明任何产品权限能力或风险阻断效果。 |
工程常见问题
提示词已经要求模型只生成允许域名,还需要代码校验吗?
需要。系统指令只能影响生成行为,不能保证每次输出符合格式与权限要求。应用仍要确定性解析并限制 scheme、host、路由和参数。
App Links 验证通过后,链接是不是就可信了?
不是。验证说明域名与应用签名有关联,不证明参数真实、对象属于当前用户,也不证明当前会话有权执行状态变更。
模型生成的链接只打开详情页,是否可以直接执行?
只读路由仍应经过解析、域名、路径和参数校验。通过后可以导航到详情入口,但页面读取对象时仍要执行当前会话和对象权限检查。
在 Deep Link 中增加 confirmed=true 能否跳过确认页?
不能。确认等级由应用路由策略决定,不能由不可信查询参数降低。高风险动作应使用应用重新生成的摘要取得明确确认。
把地址包装成不可变 PendingIntent 是否就安全?
不够。不可变设置减少后续修改,但创建前仍要校验地址,触发后还要复核当前会话、对象状态、幂等和返回栈。
评估 AI Deep Link 执行边界前应准备哪些材料?
准备模型动作类型、允许域名与路由、参数 schema、导出组件、确认等级、服务端权限、幂等方案和拒绝用例,再通过御盾中央平台提交评估需求。