先看结论与判断条件
- 为每个 WebView 页面建立内容模式、起始 URL、允许 HTTPS 主机、content authority、JavaScript、文件访问和错误页清单,不用全局默认代替页面责任。
- 普通远端页面和应用内静态资源默认不需要 file 访问;本地资源优先通过受控 HTTPS 风格映射加载,并对未映射路径保持拒绝。
- content URI 只有在明确业务需要时启用,允许 authority、路径范围、授权生命周期和读取者必须同时可审阅,不能仅凭 scheme 放行。
- 加固前后都要验证允许页面、未知主机、HTTP、file、未授权 content URI、重定向和错误页,拒绝路径是正式验收的一部分。
- JavaScript、导航、文件访问和本地凭据属于不同控制;关闭某一项不能代替 TLS、来源限制、服务端授权或 JS Bridge 专项复测。
- 所有结果绑定同一 APK、WebView 版本、设备、页面配置和业务入口,不虚构兼容率、攻击阻断或御盾未证实能力。
先把 WebView 页面按内容来源分组
WebView 文件访问问题不能用一个全局开关解释。应用可能同时包含远端帮助页、登录或支付页面、应用内离线说明、content URI 预览、下载结果页和错误页,每类入口需要的协议、脚本、认证与缓存不同。加固回归应从页面资产表开始,列出谁创建 WebView、谁设置 WebSettings、谁决定首个 URL、导航由谁拦截以及页面失败时进入哪里。
Android WebView unsafe file inclusion 指出,文件访问能力与不可信内容、JavaScript 等组合会扩大本地文件和脚本风险。因此资产表不能只记录 allowFileAccess 的最终值,还要记录 JavaScript、content 访问、允许 HTTPS 主机、重定向处理、下载和错误页。某项能力有业务理由时限定到具体页面,并保存所有者和测试入口;没有理由时默认拒绝。
本页只处理 file、content、HTTPS 和应用内资源的访问路径,不重复 JS Bridge 接口与 origin 专项。若页面暴露 Native 接口,应继续执行同站的 [WebView JS Bridge 加固回归](/zh-cn/articles/webview-js-bridge-hardening-regression/);文件访问关闭不能证明 Bridge 权限正确,Bridge 移除也不能证明未知 URL 或 content URI 已被拒绝。
| 内容模式 | 典型用途 | 默认文件访问 | 主要验收 |
|---|---|---|---|
| remote_https | 受控远端页面 | 关闭 | 主机、TLS、重定向与错误页 |
| appassets_https | 应用内静态资源映射 | 关闭 | 映射路径和未映射拒绝 |
| content_uri | 受控文档或媒体预览 | 按页最小开放 | authority、路径与授权生命周期 |
| file_url | 历史本地页面 | 迁移或严格例外 | 替代方案、隔离和到期 |
| mixed_content | HTTPS 页面加载其他协议 | 不作为文件能力 | 按正式策略拒绝或例外 |
| error_page | 离线和阻断提示 | 关闭 | 不泄露原 URL 与内部错误 |
设置清单必须落到每个 WebView 实例
同一应用可能在多个 Activity、Fragment、Dialog、SDK 或动态模块创建 WebView。只搜索一处 setAllowFileAccess 调用会漏掉 XML、封装组件、第三方 SDK 与运行时分支。清单以稳定 screenId 标识实例,连接创建入口、配置函数、contentMode、startUrl、允许主机、content authorities、JavaScript、缓存、下载处理和错误页,再从最终候选确认实际值。
配置来源也要明确。编译常量、远端配置、账号权限、地区、调试菜单和 A/B 分组都可能改变起始 URL 或脚本开关。远端配置只能从受控目录选择,不应任意下发完整 URL 后绕过 allowlist。回归记录最终解析结果和配置版本;若无法重现某一分支,状态写未测试,不能用默认分支成功代替。
设置值本身不是最终结论。WebViewClient 对导航的处理、shouldInterceptRequest 的资源代理、下载监听、页面回调和 Activity 的外部 Intent 都可能改变路径。资产表为每个控制记录“设置层”“导航层”“资源层”“业务授权层”和“设备验证层”,避免一个布尔值被误写成整页安全。
| 字段 | 回答的问题 | 通过条件 | 异常动作 |
|---|---|---|---|
| screenId | 哪个业务入口使用配置 | 稳定且唯一 | 重复或缺失即阻断 |
| contentMode | 内容属于哪类来源 | 受控枚举 | 未知模式拒绝 |
| startUrl | 首个导航目标是什么 | 与模式和 allowlist 一致 | 运行时任意值待审 |
| allowedHosts | 哪些 HTTPS 主机可访问 | 精确主机集合 | 空集合或通配待审 |
| contentAuthorities | 哪些 provider 可读取 | 逐 authority 与路径限定 | 只按 content scheme 放行 |
| errorPage | 拒绝或失败后显示什么 | 本地受控且无敏感信息 | 回显内部 URL 或堆栈 |
应用内资源优先使用受控 HTTPS 风格映射
Android 的不安全文件包含指南建议用 WebViewAssetLoader 等方式把应用内 assets 或 resources 映射到 HTTPS 风格地址,而不是依赖 file URL。这样应用可以保持文件访问关闭,并把可服务路径限制在显式注册的处理器。迁移时先列出真实离线页面、脚本、样式、字体和图片,确认所有相对引用在映射地址下仍解析,再删除旧 file 入口。
映射不是把整个应用目录公开给 WebView。每个 PathHandler 应对应可说明的资源根,未映射路径、路径规范化异常、未知扩展和越界请求保持拒绝。错误页也使用独立受控路径,不从失败 URL 拼接本地文件名。回归既检查允许的首页和子资源,也检查未登记路径返回失败且不会跳到通用文件读取。
加固可能改变 assets、resources 或模块位置,运行验证必须来自最终 APK。静态检查能确认文件存在,不能证明映射、MIME 类型、相对 URL、缓存和页面脚本在目标 WebView 版本上正常。测试在应用安装后从真实入口打开离线页面,覆盖冷启动、进程恢复、模块未安装或已安装状态,并记录加载的主文档与关键子资源。
| 对象 | 允许路径 | 拒绝路径 | 运行断言 |
|---|---|---|---|
| HTML 首页 | 登记的 assets 页面 | 未知页面和路径跳转 | 主文档完成且标题正确 |
| 脚本与样式 | 同一受控映射根 | 外部未知来源 | 关键交互和样式可用 |
| 图片与字体 | 登记资源类型 | 越界或不存在条目 | 非空并可渲染 |
| 错误页 | 固定应用内地址 | 把原 URL 当文件名 | 无敏感路径和堆栈 |
| feature 资源 | 模块安装后的映射 | 未安装时直接读取 | 门控与回退一致 |
| 缓存版本 | 与候选版本绑定 | 跨版本旧资源继续使用 | 升级后内容一致 |
content URI 访问需要 authority 与生命周期约束
content URI 常用于受控文档、媒体或应用组件间共享,但 content scheme 本身不表示资源可信。允许读取时至少核对 authority、路径或文档身份、调用来源、授权方式、生命周期和可接受 MIME 类型。页面只应取得当前业务动作需要的最小对象,不把 content 访问作为浏览整个 provider 的通用能力。
加固回归要覆盖合法授权、未授权 authority、授权撤销、进程重建、文件删除、类型不匹配和 provider 返回错误。合法内容应按产品契约显示;拒绝时进入受控错误页,不能崩溃、无限重试或继续使用已经失效的缓存内容。测试使用公开安全样本,不写真实用户文件,不输出内部路径或 provider 数据。
若 JavaScript 同时开启,应单独说明页面为何需要脚本以及脚本可以接触哪些内容。Android WebView unsafe file inclusion 对文件能力和脚本组合给出风险边界;工程上应尽量避免不可信脚本接触本地内容。关闭 JavaScript 能缩小一部分风险,但仍不能替代 authority、路径、授权和导航限制。
| 场景 | 前置条件 | 预期结果 | 证据 |
|---|---|---|---|
| 合法预览 | 允许 authority 与有效授权 | 只显示目标对象 | URI 摘要、类型和页面结果 |
| 未知 authority | 未登记 provider | 拒绝并显示安全错误 | 拒绝原因和无加载记录 |
| 授权撤销 | 原授权已失效 | 停止读取且不显示旧敏感内容 | 撤销时间线和缓存状态 |
| 类型不符 | MIME 不在允许集合 | 拒绝渲染 | 声明类型和实际结果 |
| 对象删除 | URI 已不存在 | 可理解错误且无重试环 | provider 错误和 UI |
| 进程恢复 | 仅有持久或临时授权 | 按真实授权重新判断 | 重建后访问结果 |
远端页面要验证主机、重定向、TLS 与错误页
Build Android WebView apps 说明 WebView 的 JavaScript、导航和 Native 交互需要明确可信内容与线程边界。远端页面入口使用 HTTPS,不只核对初始 URL,还要对每次主文档导航和必要子资源应用既定策略。主机判断使用解析后的 scheme 与 host,不使用 startsWith 等字符串前缀;端口、用户信息、大小写、尾点和重定向按照统一规范化规则处理。
允许主机列表要表达业务所有权,而不是宽泛顶级域。帮助、账号、支付和活动页面可以有不同集合,远端配置只能引用登记 ID。重定向到未知主机、HTTP 或其他 scheme 时拒绝并进入受控错误页;需要外部浏览器处理的链接也要有明确用户动作和允许 scheme,不让 WebView 自行加载未知来源。
错误页是安全和可用性共同边界。它应说明网络失败、内容不可用或访问被阻断,不回显完整敏感 URL、请求头、令牌、内部文件路径或调试堆栈。重试只回到经过重新校验的原始业务入口,不直接使用错误回调里的任意地址。加固前后使用同一网络条件和服务器测试资源,区分 TLS、DNS、HTTP、策略拒绝和渲染失败。
- 主文档每次导航都解析 scheme 与 host
- 重定向后的目标重新执行 allowlist
- HTTP、未知 scheme 与未知主机进入拒绝路径
- 外部浏览器跳转需要明确用户动作和策略
- 错误页不显示敏感 URL、令牌或内部路径
- 重试从受控业务入口重新解析目标
用配置验证器生成允许和拒绝用例
自动门禁读取页面配置快照,先验证字段完整和相互约束,再为每个 screenId 生成运行用例。remote_https 要求 HTTPS 起始地址且 host 在精确允许集合;appassets_https 要求受控应用内主机;content_uri 要求 content 访问开启、authority 已登记且脚本默认关闭;所有模式都要求 file 访问关闭。例外不应藏在代码分支里,而应有所有者、原因和到期。
下面 Python 示例只读 JSON 配置并输出测试矩阵,不发起网络请求、不读取 content 数据,也不包含真实域名或文件路径。它验证页面身份、模式、URL、主机与设置,然后生成允许起始页以及 file、未知 content authority、HTTP 和未知 HTTPS 主机四类拒绝用例。输出供 instrumented test 驱动真实 WebView;生成用例本身不是设备通过回执。
项目可以加入重定向、错误页、进程恢复和配置版本字段,但不要把生产 Cookie、令牌、API Key 或用户 URI 放入清单。若配置门禁失败,先修正页面所有权或策略;不要在测试器里自动放宽。最终运行结果还要证明 WebView 拒绝了负向目标且允许页面的关键子资源可用,不能只断言验证器成功输出 JSON。
- 配置快照不包含 Cookie、令牌或用户 URI
- screenId 唯一并连接真实业务入口
- 所有内容模式默认关闭 file 访问
- HTTPS 主机和 content authority 使用精确集合
- 每个页面同时生成允许与拒绝用例
- 设备测试保存实际导航和最终页面结果
from pathlib import Path
from urllib.parse import urlparse
import json
import sys
if len(sys.argv) != 2:
raise SystemExit("usage: build_webview_matrix.py webview-screens.json")
source = Path(sys.argv[1])
if not source.is_file():
raise SystemExit("configuration file does not exist")
payload = json.loads(source.read_text(encoding="utf-8"))
screens = payload.get("screens")
if not isinstance(screens, list) or not screens:
raise SystemExit("screens inventory is required")
allowed_modes = {"remote_https", "appassets_https", "content_uri"}
seen = set()
failures = []
cases = []
for screen in screens:
screen_id = screen.get("screenId")
mode = screen.get("contentMode")
start_url = screen.get("startUrl", "")
parsed = urlparse(start_url)
hosts = set(screen.get("allowedHttpsHosts", []))
authorities = set(screen.get("contentAuthorities", []))
if not screen_id or screen_id in seen:
failures.append(f"invalid or duplicate screenId: {screen_id!r}")
continue
seen.add(screen_id)
if mode not in allowed_modes:
failures.append(f"{screen_id}: unsupported content mode")
if screen.get("allowFileAccess") is not False:
failures.append(f"{screen_id}: file access must be disabled")
if mode == "remote_https":
if parsed.scheme != "https" or parsed.hostname not in hosts:
failures.append(f"{screen_id}: remote start URL is outside its HTTPS allowlist")
elif mode == "appassets_https":
if parsed.scheme != "https" or parsed.hostname != "appassets.androidplatform.net":
failures.append(f"{screen_id}: appassets URL is not on the controlled host")
elif mode == "content_uri":
if parsed.scheme != "content" or parsed.netloc not in authorities:
failures.append(f"{screen_id}: content URI authority is not allowed")
if screen.get("allowContentAccess") is not True:
failures.append(f"{screen_id}: content mode requires explicit content access")
if screen.get("javascriptEnabled") is not False:
failures.append(f"{screen_id}: content preview must disable JavaScript")
cases.append({"screenId": screen_id, "expect": "allow", "url": start_url})
cases.extend([
{"screenId": screen_id, "expect": "deny", "url": "file:///blocked-test.html"},
{"screenId": screen_id, "expect": "deny", "url": "content://unapproved.test/item"},
{"screenId": screen_id, "expect": "deny", "url": "http://cleartext.test/page"},
{"screenId": screen_id, "expect": "deny", "url": "https://unapproved.test/page"},
])
if failures:
print("WebView configuration gate failed:", file=sys.stderr)
for failure in failures:
print(f"- {failure}", file=sys.stderr)
raise SystemExit(2)
print(json.dumps({"cases": cases}, ensure_ascii=False, indent=2))设备回归必须证明拒绝路径真实生效
Android instrumented tests 可以在设备或模拟器上访问 Android 运行时、组件和系统 API,适合驱动最终候选的 WebView。每个页面从真实 Activity 或 Fragment 入口创建,记录 WebView 与系统版本、设置快照、初始 URL、导航事件、错误回调和最终页面。允许用例验证主文档与关键资源,拒绝用例验证没有显示目标内容且进入受控错误状态。
回归至少覆盖冷启动、进程恢复、前后台切换、网络中断、缓存存在与清理后状态,以及应用实际支持的代表 API 与厂商 WebView 版本。单一设备通过不能代表所有版本;同一系统也可能具有不同 WebView 组件。报告列出真实覆盖、失败、未执行和基础设施错误,不把测试任务启动或页面无崩溃写成通过。
加固前后比较必须固定页面配置、测试服务器内容、账号、设备条件和业务入口。若加固候选失败,先寻找最早差异:设置值、首个 URL、导航拦截、资源代理、content 授权、页面回调或渲染。一次只调整一项并生成新候选,避免同时开启 file 访问、扩大主机 allowlist 和关闭优化,使问题消失却无法解释。
| 场景 | 输入 | 关键断言 | 证据边界 |
|---|---|---|---|
| 远端允许页 | 登记 HTTPS 主机 | 页面和关键子资源可用 | 只覆盖当前服务器内容 |
| 未知导航 | HTTP、未知主机或 scheme | 拒绝且显示安全错误 | 不代表全部 URL 组合 |
| 应用内资源 | 登记 appassets 路径 | 主文档与相对资源可用 | 未映射路径另测 |
| content 预览 | 有效 authority 与授权 | 只显示目标对象 | 使用公开安全样本 |
| 授权失效 | 撤销或对象删除 | 停止显示并可恢复 | 记录缓存与进程状态 |
| 进程重建 | 已打开或失败页面 | 重新执行策略而非复用旧布尔值 | 只覆盖列出入口 |
服务端凭据与发布证据保持独立边界
Android insecure API usage 说明静态放在移动客户端的 API Key 可能被逆向或拦截,敏感服务的长期凭据应保留在服务端。WebView 页面地址被 allowlist 接受,不代表可以把高权限密钥写入 URL、脚本、Header 或本地文件。服务端代理还要验证用户、受众、权限、速率和审计,客户端加固不能把公开页面提升为可信凭据存储。
OWASP MASVS-RESILIENCE 将抗逆向与抗篡改放在纵深防御范围。加固可以增加选定客户端逻辑被理解和修改的成本,但不能替代 WebView 来源策略、TLS、content 授权、服务端认证和完整发布链。关闭 file 访问也只是一个控制;若页面仍允许未知 HTTPS 主机、危险重定向或过宽 content authority,整体边界仍不完整。
放行记录应限定为指定 APK 摘要、页面配置版本、WebView 版本、设备和列出的允许与拒绝路径已完成回归,不能写成文件永不泄露、所有网页安全或攻击无法成功。准备 WebView 文件访问评估时,可整理最终 APK、页面与来源清单、设置快照、错误页、content authority、测试服务器和设备矩阵,再通过御盾中央平台提交申请。
| 证据 | 必须绑定 | 支持的结论 | 禁止外推 |
|---|---|---|---|
| 候选身份 | APK 摘要、签名与版本 | 测试对象明确 | 同名文件等同 |
| 页面清单 | screenId、模式和所有者 | 配置范围完整 | 未登记 WebView 也安全 |
| 设置快照 | 候选和运行分支 | 实际布尔值与来源集合 | 一个开关代表整页 |
| 导航回执 | URL 类别、设备和结果 | 列出允许与拒绝路径 | 所有 URL 已覆盖 |
| content 回执 | authority、授权和样本 | 列出对象按契约处理 | 用户数据永不泄露 |
| 设备矩阵 | 系统与 WebView 版本 | 列出组合通过或失败 | 全部厂商永久兼容 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| WebView 文件访问、JavaScript 与不可信内容组合会扩大本地文件和脚本风险。 | Android WebView unsafe file inclusion 描述相关设置、风险与应用内资源加载建议。 | 关闭文件访问不能替代 URL allowlist、TLS、content 授权和 Bridge 权限。 |
| WebView 的 JavaScript、导航和 Native 交互需要明确可信内容与线程边界。 | Build Android WebView apps 描述 WebView 加载、导航、JavaScript 与应用交互。 | 页面能加载不证明缓存、重定向、来源限制或业务授权正确。 |
| WebView API 对 JavaScript 接口、线程和版本行为提供平台约束。 | Android WebView API 记录 WebView 类与相关接口行为。 | API 文档不覆盖应用自定义协议、页面 allowlist 和服务端授权实现。 |
| 移动客户端中的静态 API Key 可能被逆向或拦截。 | Android insecure API usage 描述不安全 API 使用与客户端凭据边界。 | 把长期凭据移到服务端后仍需用户认证、限权、审计和滥用控制。 |
| 依赖真实 Android WebView、Context 和组件生命周期的行为适合设备端测试。 | Android instrumented tests 描述在设备或模拟器上访问 Android 框架的测试。 | 单一设备和 WebView 版本通过不能代表全部 API、厂商和组件版本。 |
| 抗逆向与抗篡改属于移动端纵深防御控制。 | OWASP MASVS-RESILIENCE 列出相关韧性控制目标。 | 控制目录不证明某个加固候选达到特定强度,也不能替代来源和授权策略。 |
| 每个 WebView 页面应按内容模式维护独立设置、允许来源和拒绝用例。 | 工程判断:远端页面、应用内资源与 content 预览的信任和失败边界不同。 | 具体允许主机、authority、脚本和回退必须由项目业务证据确认。 |
| 配置验证、允许路径和拒绝路径必须绑定同一最终候选与设备条件。 | 工程判断:只有同候选回执才能排除远端配置、重构建和 WebView 版本漂移。 | 列出路径通过不能外推所有 URL,也不能证明用户数据不存在其他暴露面。 |
工程常见问题
加固后 WebView 页面能打开,是否说明文件访问安全?
不能。还要验证设置、允许主机、重定向、file 与 content 拒绝路径、错误页、进程恢复和真实业务授权。
应用内 HTML 是否必须使用 file URL?
通常可以优先使用受控 HTTPS 风格的应用内资源映射,并关闭 file 访问。历史例外需限定页面、原因、所有者和到期。
允许 content URI 后能否读取任意 provider?
不能。应限定 authority、路径或对象、授权生命周期和类型,并覆盖未知 authority、撤销、删除与进程重建。
关闭 JavaScript 是否可以替代文件访问限制?
不能。关闭脚本只缩小一类风险,仍需限制 file、content、HTTPS 主机、重定向、错误页和服务端授权。
WebView 文件访问回归需要哪些设备条件?
至少记录目标 API、设备型号、系统 WebView 版本、网络条件、缓存和进程状态;按实际支持范围扩展矩阵。
申请 WebView 文件访问加固评估要准备什么?
准备最终 APK、WebView 页面与来源清单、设置快照、错误页、content authority、测试服务器、允许与拒绝用例及设备矩阵,再通过御盾中央平台提交申请。