鸿蒙 PC Markdown 编辑器预览安全:CSP、DOMPurify 与权限收缩

本文从不可信 Markdown 的威胁模型出发,说明渲染、DOM、URL、ArkWeb 能力和原生 Bridge 如何形成分层防护。完整示例代码:https://gitcode.com/VON-/codex_md_oh

Markdown 预览为什么是安全边界

Markdown 编辑器通常会把文本转成 HTML。如果直接将渲染结果赋给 innerHTML,用户打开的文档就可能携带脚本、事件属性、表单、iframe 或危险链接。在鸿蒙 PC 端,预览还运行在与原生 Bridge 相邻的 ArkWeb 环境中,因此必须采用多层防护,不能只依赖某一个正则替换。

第一层:用 CSP 默认禁止外部能力

ArkWeb 加载的 HTML 内置了如下 CSP:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src data: resource: file:; font-src data:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'"
/>

代码来源:web-editor/index.html

default-src 'none' 建立默认拒绝策略,connect-src 'none' 禁止 Web 网络连接,object-src 'none'base-uri 'none'form-action 'none' 分别阻止对象嵌入、基地址篡改和表单提交。

当前为了支持单 HTML 内联包,script-srcstyle-src 仍包含 'unsafe-inline'。这是已知折中,不应被描述为理想的最终 CSP。后续可以评估 hash/nonce 或安全的多资源加载策略。

第二层:渲染器不接受原始 HTML

const markdownRenderer = new MarkdownIt({
  html: false,
  linkify: true,
  typographer: false,
  breaks: false
});

代码来源:web-editor/src/main.ts

html: false 使 Markdown 文档中的原始 HTML 不会被当成可执行页面结构。但是安全设计不能假定渲染器永远无缺陷,因此仍需要输出净化。

第三层:DOMPurify 净化与链接隔离

function sanitizeMarkdown(content: string): string {
  const unsafeHtml = markdownRenderer.render(content);
  return DOMPurify.sanitize(unsafeHtml, {
    USE_PROFILES: { html: true },
    FORBID_TAGS: ['style', 'iframe', 'object', 'embed', 'form'],
    FORBID_ATTR: ['style']
  });
}

function renderPreview(content: string): void {
  preview.innerHTML = sanitizeMarkdown(content);

  preview.querySelectorAll<HTMLAnchorElement>('a').forEach((link) => {
    link.target = '_blank';
    link.rel = 'noopener noreferrer';
    link.addEventListener('click', (event) => event.preventDefault());
  });
  previewDirty = false;
}

代码来源:web-editor/src/main.ts

DOMPurify 使用 HTML 白名单模式,另外禁止可以嵌入外部上下文的标签和行内样式。预览区链接补上 noopener noreferrer,并在当前技术验证中默认拦截点击,避免文档内容直接发起跳转。

鸿蒙 PC 离线运行截图

安全边界不会在界面上额外显示提示。下图是内联编辑器在鸿蒙 PC / 2in1 模拟器中的离线运行载体:ArkWeb 无在线图片、文件或定位权限,只暴露受限 Bridge。

在这里插入图片描述

已验证与未验证项

当前 14 项 Playwright 回归已经覆盖 Markdown 中的 <script>、危险链接、导出标题注入、独立 HTML、打印媒体 DOM 和生产单文件包。预览与导出共用 sanitizeMarkdown,避免两条渲染链路出现安全规则漂移。

鸿蒙原生侧已经接入 DocumentViewPicker、WebPrintDocumentAdapter 和 Print Kit,并保持在线图片、文件访问、地理位置与 DOM Storage 关闭。系统 PDF 产物、本地图片真实 URI、只读目标和空间不足仍需设备测试,因此当前只能判断核心边界成立,不能推导为所有场景无风险。

先把 Markdown 当作不可信输入

本地文件不等于可信文件。Markdown 可能来自代码仓库、邮件附件、聊天工具、下载目录或第三方同步盘,作者与打开文件的用户并不一定相同。攻击者可以把恶意内容伪装成普通文档,等待用户在拥有本地文件权限的编辑器中打开。

威胁模型至少包含:

  • 原始 HTML 中的 <script>、事件属性和可执行 URL。
  • iframeobject、表单等加载外部上下文或提交数据的节点。
  • Markdown 链接使用 javascript:data: 等危险协议。
  • 图片和样式触发网络请求,泄露文档打开时间、IP 或标识。
  • 导出路径绕过预览净化,生成可执行的独立 HTML。
  • 预览内容借助 Bridge 调用原生文件、打印或保存能力。
  • 超大或特殊结构文档造成解析、DOM 和布局资源耗尽。

安全目标不是让 Markdown 支持所有浏览器能力,而是在保证常用排版的前提下默认拒绝执行、连接、提交和越权文件访问。

多层防护为什么不能合并成一层

Markdown 渲染器、DOMPurify、CSP、ArkWeb 配置和 Bridge 白名单分别保护不同阶段:

不可信 Markdown
  → markdown-it(禁用原始 HTML)
  → DOMPurify(净化生成 HTML)
  → 受控 DOM 插入与链接处理
  → CSP(限制页面资源和连接)
  → ArkWeb 权限收缩(限制 Web 系统能力)
  → Bridge 白名单(限制原生入口)

渲染器配置可以减少危险输入面,但无法保证所有插件未来都安全;DOMPurify 处理输出 DOM,却不能阻止应用自己的脚本主动发起连接;CSP 限制浏览器加载,却不决定原生代理暴露什么;Bridge 白名单保护原生方法,却不负责清理 HTML。分层设计使单点失误不直接变成完整权限突破。

CSP 要从默认拒绝开始

default-src 'none' 的意义是新增资源类型默认被阻止,而不是默认继承一个宽松来源。随后只为当前确实需要的能力开放最小集合:脚本和样式来自内联生产包,图片只允许 data、resource、file,连接明确为 none,对象、base 和表单均禁止。

当前单 HTML 包要求 'unsafe-inline',这是风险折中。它意味着 CSP 不能阻止页面内被注入的任意内联脚本,因此安全性更依赖构建资源完整性和 DOM 净化。更强方案可以考虑基于 hash 的 script-src、分离的本地只读资源或 ArkWeb 支持的 nonce 机制,但需要验证 rawfile 加载和生产构建是否稳定。

CSP 不应只写在模板里就算完成。生产打包可能重写 HTML,测试应加载最终 rawfile/editor/index.html,检查策略仍存在,并验证没有外部脚本、样式和连接。

禁用原始 HTML仍需净化输出

markdown-ithtml: false 会把用户写下的原始 HTML 当作文本,这已经阻断大量直接注入。但 Markdown 本身仍能生成链接、图片等 HTML,未来插件还可能引入表格、脚注、任务列表或数学公式。渲染器和插件中的漏洞也不能被排除。

DOMPurify 位于渲染后、插入前,使用 HTML profile 并额外禁止 style、iframe、object、embed 和 form。禁用行内 style 可以减少 CSS 覆盖和外部资源技巧,但会限制用户自定义排版;这是本地文档安全优先于任意 HTML 兼容的产品选择。

净化结果必须作为唯一可插入预览 DOM 的内容。不要在另一个功能中直接调用 markdownRenderer.render() 后赋给 innerHTML,也不要先插入再清理;危险节点一旦进入活动文档,某些副作用可能已经发生。

URL 协议需要单独审查

HTML 标签安全不代表 URL 安全。链接和图片的 hrefsrc 可以携带协议,Markdown 相对路径还会根据页面 base URL 解析。当前预览为链接设置 _blanknoopener noreferrer 并拦截点击,避免文档直接导航。

以后若允许打开链接,应由受控处理器解析 URL,建立协议白名单:常规网页可允许 https,本地文件和自定义协议需要用户确认,javascript 必须拒绝。仅用 startsWith('http') 不够,应使用标准 URL 解析并处理大小写、空白、编码和相对路径。

图片更复杂。在线图片会产生隐私请求,因此 ArkWeb 的 onlineImageAccess(false) 保持关闭;本地相对图片需要把文档目录授权映射为受控资源,而不能开启 Web 任意文件访问。导出 HTML 时还要决定嵌入 data URI、复制资源还是保留相对引用,每一种都要有大小和路径边界。

Bridge 不能被预览内容驱动

原生代理与页面同处一个 JavaScript 环境,因此必须确保净化后的预览节点无法自动触发原生命令。Bridge 只暴露少量白名单方法,文件 URI 永远不传给预览 DOM,保存与打印由应用 UI 的明确动作发起。

通用 onCommand(command, content) 仍需在原生侧限制命令枚举、内容长度和当前状态。不能因为 Web 页面随 HAP 打包就跳过校验:一旦预览漏洞允许执行页面脚本,攻击代码看到的就是同一个 Bridge。原生层必须成为第二道权限边界。

更高风险的能力,例如打开外部链接、读取本地图片、插件 API 和网络同步,应使用更窄的专用协议,并附带用户手势或授权上下文。不要不断给现有通用命令添加字符串分支。

预览与导出必须共享净化结果

导出是最容易被遗漏的第二入口。用户在应用预览中没有看到危险内容,并不代表导出的 HTML 安全。如果导出直接使用未净化渲染结果,文件在普通浏览器打开时可能获得更宽松的权限。

当前 sanitizeMarkdown 同时供预览、HTML 导出和打印准备使用。独立 HTML 还包含自己的严格 CSP,标题经过文本转义,页面不加载在线脚本、样式或字体。打印页面则复用已经净化的预览 DOM,通过媒体样式隐藏编辑器。

共享函数还不够,测试必须分别执行三个入口。预览测试检查活动 DOM 没有脚本和副作用;导出测试解析返回字符串,检查标题、危险 URL和独立 CSP;打印测试检查打印模式只保留安全正文。

资源耗尽也是安全问题

安全不仅是脚本执行。恶意 Markdown 可以包含数十万级嵌套标记、超长链接、巨大 data URI 或使解析器退化的结构,导致 ArkWeb 内存激增、页面无响应甚至应用退出。文档大小上限和大文件模式因此也是安全防护。

渲染前应限制输入规模,大文档关闭实时预览;未来插件要限制递归、节点数和输出大小;图片 data URI 需要单项上限。打印和导出不能绕过这些保护,否则用户虽然能编辑大文件,却在点击导出时触发峰值。

性能故障的错误信息不应包含完整恶意内容,日志也不能记录用户文档。只记录输入大小、阶段、错误类型和匿名诊断即可。

安全测试要验证副作用

仅检查字符串中没有 <script> 不充分。攻击载荷可以通过事件属性、SVG、URL 或解析差异触发。测试应把净化结果放入真实 DOM,再检查危险节点、属性、导航和全局副作用。

建议载荷集合包括:

类别 示例目标 预期
脚本标签 script 作为文本或被移除,不执行
事件属性 onerroronclick 属性不存在
危险 URL javascript: 不可导航
嵌入上下文 iframe/object/embed 节点不存在
表单 form/action 不可提交
样式 style 属性和标签 不进入预览
网络图片 https 图片 ArkWeb 不发起在线加载
标题注入 </title> 独立 HTML 标题被转义
导出差异 预览安全、导出危险 三条链路结果一致
资源耗尽 大 data URI/深层结构 受大小或降级策略限制

安全依赖升级后必须重新执行这些回归。DOMPurify、markdown-it、ArkWeb 或 CSP 配置变化都可能改变结果。

隐私与可观测性的平衡

为了排查安全问题,应用需要知道预览或导出在哪个阶段失败,但不应上传或打印用户 Markdown。诊断可以包含版本、HAP 哈希、文档大小、节点数量、错误类别和发生时间,不包含正文、文件路径或 URL 查询参数。

如果未来加入崩溃上报和遥测,默认应脱敏并允许用户关闭。安全扫描可以在本地执行,只返回规则编号和位置范围。鸿蒙 PC 编辑器处理的常常是代码、内部文档和个人笔记,隐私边界本身就是产品可信度的一部分。

预览安全审查清单

  • 所有 Markdown 默认不可信,包括本地和工作区文件。
  • markdown-it 禁用原始 HTML,插件引入前单独做安全评估。
  • DOMPurify 位于渲染后、DOM 插入前,预览与导出共用策略。
  • CSP 使用默认拒绝,明确禁止 connect、object、base 和 form。
  • 'unsafe-inline' 被记录为折中,并有未来收紧路径。
  • ArkWeb 关闭在线图片、文件访问、定位和不必要存储。
  • 链接和图片协议经过标准解析与白名单,不用字符串猜测。
  • Bridge 只暴露最小方法,高风险命令在原生侧再次校验。
  • 大文档、data URI 和输出 DOM 有资源上限或降级策略。
  • 测试同时覆盖预览、独立 HTML、打印和生产单文件包。
  • 诊断不记录正文、敏感 URI 和可识别的文档内容。

安全预览不是一个净化函数,而是一条从文本输入到系统能力的完整边界。只有渲染器、DOM、浏览器策略、ArkWeb 权限、Bridge 和导出保持一致的默认拒绝,鸿蒙 PC 编辑器才能放心打开来源复杂的本地 Markdown。

Markdown 插件必须进入同一威胁模型

任务列表、脚注、数学公式、Mermaid 和代码高亮会丰富预览,也会引入新解析器、DOM 结构、样式和资源。有些插件直接输出 HTML,有些运行 JavaScript 修改 DOM,有些加载 Web Worker 或字体。安装插件不能只看渲染效果。

评估时需要回答:输入是否可导致脚本执行,输出是否再次经过 DOMPurify,是否需要放宽 CSP,是否联网,是否访问文件,是否存在复杂度攻击,依赖是否持续维护。必须放宽 connect-src 或开放 unsafe-eval 的插件风险很高,应优先寻找纯解析、可离线、输出可净化的方案。

插件配置作为安全代码审查,版本锁定并运行专用载荷。插件失败时预览降级为普通代码块或文本,不影响源码编辑和保存。

代码高亮的安全与性能边界

代码高亮器通常把代码文本解析成带 class 的 span。它不应信任语言名称生成任意 class/属性,也不能把代码块内容作为 HTML。语言包数量会增加 HAP 体积和初始化成本,自动检测在大代码块上还可能消耗大量 CPU。

安全策略可以限定支持语言集合,未知语言按纯文本显示;输出统一经过净化;大文档或大代码块关闭高亮。主题 CSS 使用应用内静态样式,不从 CDN 获取。

测试包括闭合标签、超长 token、恶意语言名和嵌套标记,确认代码仍作为文本显示,导出与打印结果一致。

本地图片解析需要路径沙箱

允许 Markdown 显示相对图片是桌面编辑器的重要能力,但打开 ArkWeb fileAccess 会扩大到未经授权路径。更安全的架构由原生侧在当前文档或工作区授权范围内解析资源,请求时校验规范化路径仍位于根目录,再通过受控资源 URL、数据流或临时只读映射提供给页面。

路径验证要处理 ..、百分号编码、大小写、符号链接、绝对路径和不同 URI scheme。仅检查字符串前缀不可靠。资源还要限制类型和大小,SVG 可能包含脚本或外部引用,默认可当作不可信文档进一步净化或禁用。

文档切换后旧资源授权应失效,不能让新文档继续访问上一目录缓存。导出复制图片时复用同一边界。

剪贴板与拖放也是输入通道

用户从网页复制内容时,剪贴板可能同时包含纯文本和 HTML。源码 Markdown 编辑器默认插入纯文本最可预测;若未来支持“粘贴为 Markdown”,转换器必须把 HTML 当作不可信输入,限制图片下载和链接协议,并让用户预览结果。

文件拖放可能携带路径、URI 或目录。Web 页面不直接读取拖入文件,原生侧根据系统授权创建打开请求。把 HTML 片段直接拖入预览区也不应成为 DOM 注入入口。

剪贴板写出同样需要用户动作,不允许文档预览自动复制内容。敏感权限和系统提示在鸿蒙 PC 真机验证。

外部链接需要用户手势与中间确认

当前预览阻止所有链接点击,是技术验证期最安全策略。正式产品若开放,应只响应真实用户点击,解析后显示目标域名和协议,危险或未知协议拒绝。本地 file:、应用自定义 scheme 和命令链接的权限高于普通 https,需要独立确认。

打开链接最好交给系统默认浏览器,不在带原生 Bridge 的同一 ArkWeb 中导航。新窗口也要使用 noopener 语义,避免外部页面获得原页面引用。网络访问发生在系统浏览器后,编辑器本身仍保持离线边界。

链接文案可能伪装域名,确认界面应显示解析后的真实目标,而不是 Markdown 文本。

CSP 违规与安全日志

浏览器环境可以提供 CSP 违规信息,但 ArkWeb 对报告能力的支持需要验证。开发版本可记录被阻止的资源类型和指令,帮助发现构建误配置;正式版本不能把完整 URL、文档内容或内联脚本片段上报。

若无法使用自动报告,生产包测试仍可通过加载页面、监控网络、检查控制台和断言功能来发现违规。CSP 报告是诊断辅助,不是安全策略本身。

安全日志采用规则编号、页面版本、HAP 哈希和结果码。重复恶意载荷只聚合次数,避免攻击者用文档内容污染日志或耗尽磁盘。

依赖漏洞响应流程

发现 markdown-it、DOMPurify、ArkWeb 或相关插件漏洞时,先确认受影响版本和触发条件,再制作最小载荷在当前生产 HAP 复现。若影响预览或导出,优先临时禁用相关语法/功能并发布修复,不等待大版本升级。

升级依赖后运行全部安全夹具、生产单文件审计和设备冒烟。补丁若要求放宽 CSP或打开新权限,需要更高等级评审。无法立即升级时,记录当前配置是否已阻断触发路径,并设置明确截止时间。

对已导出的 HTML,要判断漏洞是否存在于静态内容还是运行库。当前独立 HTML 不带脚本,能缩小后续影响;这也是避免在导出文件嵌入大型运行时的好处。

安全功能也要有可用的失败方式

净化器异常、预览超限或本地图片被拒绝时,源码编辑必须继续可用。界面显示预览不可用或资源被阻止,不清空文档、不尝试未经授权的替代路径。安全策略不能通过崩溃实现拒绝。

用户可以查看被阻止资源的原因,但不提供“一键永久关闭全部保护”。针对可信工作区的高级选项也应细分能力、明确风险,并默认关闭;脚本执行和任意 Bridge 调用不应成为普通文档设置。

可用的失败方式让用户更少为了完成工作而绕过安全边界。

安全边界需要负向能力清单

除了列出支持什么,还应明确普通 Markdown 永远不能做什么:执行脚本、发起网络请求、读取任意本地文件、提交表单、自动打开外部应用、调用保存/打印、写剪贴板和持久化 Web 存储。负向清单让新增功能评审更直接,任何需求触碰这些能力都必须单独授权和建模。

设备测试可通过网络监控、权限配置和恶意文档验证这些“不发生”的行为。仅观察页面没有弹窗不够,后台请求和 Bridge 调用也要断言为零。

安全默认与可信工作区

用户可能希望在自有文档中使用更多 HTML,但“可信工作区”不能成为关闭所有保护的总开关。信任应绑定明确目录、能力和时间,允许单独开启本地图片或外部链接,不开放脚本与任意原生方法。工作区移动、授权失效或文件来自外部时重新评估。

信任状态显示在应用设置或状态区,导出仍使用安全默认,除非用户明确选择高级格式。任何例外都要可撤销,并在升级后保持最小权限。

安全审查覆盖生成产物

源码配置正确不代表最终 HTML 正确。Vite 单文件插件可能内联新脚本、依赖可能加入动态代码、压缩可能改变 CSP hash。每个候选 HAP 解包或直接检查 rawfile,加载生产页面执行恶意夹具,并确认 ArkWeb 权限与 Bridge methodList 未扩大。

签名保护发布后的 HAP 完整性,构建前的依赖和生成过程仍需锁文件、物料清单与 CI 隔离。安全边界从源码一直延伸到用户实际安装的字节。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐