先看结论与判断条件

  • 为每个 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 已被拒绝。

WebView 内容模式的初始分类
内容模式典型用途默认文件访问主要验收
remote_https受控远端页面关闭主机、TLS、重定向与错误页
appassets_https应用内静态资源映射关闭映射路径和未映射拒绝
content_uri受控文档或媒体预览按页最小开放authority、路径与授权生命周期
file_url历史本地页面迁移或严格例外替代方案、隔离和到期
mixed_contentHTTPS 页面加载其他协议不作为文件能力按正式策略拒绝或例外
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 都可能改变路径。资产表为每个控制记录“设置层”“导航层”“资源层”“业务授权层”和“设备验证层”,避免一个布尔值被误写成整页安全。

页面级 WebView 配置字段
字段回答的问题通过条件异常动作
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、路径、授权和导航限制。

content URI 的允许与拒绝场景
场景前置条件预期结果证据
合法预览允许 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 使用精确集合
  • 每个页面同时生成允许与拒绝用例
  • 设备测试保存实际导航和最终页面结果
校验 WebView 文件访问配置并生成回归矩阵
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 和关闭优化,使问题消失却无法解释。

WebView 文件访问的最小设备矩阵
场景输入关键断言证据边界
远端允许页登记 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、测试服务器、允许与拒绝用例及设备矩阵,再通过御盾中央平台提交申请。

想用自己的 App 验证?

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

继续阅读: App 加固 PoC 如何形成发布结论