鸿蒙 PC Markdown 编辑器预览安全:CSP、DOMPurify 与权限收缩
鸿蒙 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-src 与 style-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。 iframe、object、表单等加载外部上下文或提交数据的节点。- 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-it 的 html: false 会把用户写下的原始 HTML 当作文本,这已经阻断大量直接注入。但 Markdown 本身仍能生成链接、图片等 HTML,未来插件还可能引入表格、脚注、任务列表或数学公式。渲染器和插件中的漏洞也不能被排除。
DOMPurify 位于渲染后、插入前,使用 HTML profile 并额外禁止 style、iframe、object、embed 和 form。禁用行内 style 可以减少 CSS 覆盖和外部资源技巧,但会限制用户自定义排版;这是本地文档安全优先于任意 HTML 兼容的产品选择。
净化结果必须作为唯一可插入预览 DOM 的内容。不要在另一个功能中直接调用 markdownRenderer.render() 后赋给 innerHTML,也不要先插入再清理;危险节点一旦进入活动文档,某些副作用可能已经发生。
URL 协议需要单独审查
HTML 标签安全不代表 URL 安全。链接和图片的 href、src 可以携带协议,Markdown 相对路径还会根据页面 base URL 解析。当前预览为链接设置 _blank、noopener 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 |
作为文本或被移除,不执行 |
| 事件属性 | onerror、onclick |
属性不存在 |
| 危险 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 隔离。安全边界从源码一直延伸到用户实际安装的字节。
更多推荐


所有评论(0)