news 2026/10/15 11:55:48

defuddle 中 Markdown 图片 alt 文本的方括号转义与安全转换机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
defuddle 中 Markdown 图片 alt 文本的方括号转义与安全转换机制
  • 后端

【免费下载链接】defuddle

Get the main content of any page as Markdown.

项目地址:https://gitcode.com/gh_mirrors/de/defuddle
点击查看免费下载

导读

本文围绕 defuddle 项目的 security--markdown-image-escaping fixture 展开,讲解当 HTML 图片的alt描述文本中含有字面方括号(如Photo [one])时,defuddle 如何将其转换为语法安全、语义完整的 Markdown 图片语法(![Photo \[one\]](...)),并梳理背后的转义管线、危险 URL 过滤与回归测试验证。读完本文,你将理解 defuddle 的 Markdown 输出层如何保证图片文本既不破坏 Markdown 语法,也不被解析成超链接或注入恶意内容。

问题背景:方括号是 Markdown 链接语法的保留字符

在 Markdown 中,图片语法alt text的 alt 描述部分由方括号界定。若 alt 文本本身包含字面方括号[one],渲染器会把它当作嵌套链接文本的开始/结束标记,从而产生歧义:

  • ![Photo [one]](https://example.com/photo.png)中的[one]可能被解析为嵌套链接的文本;
  • 更危险的是,若攻击者能在 alt 中注入](javascript:...)之类的结构,可能把 Markdown 输出重新解析成可点击的危险链接或脚本载体。

defuddle 的 fixture security--markdown-image-escaping.html 构造了这样一个最小案例:

<img src="https://example.com/photo.png" alt="Photo [one]">

对应的期望输出(即 security--markdown-image-escaping.md)是:

![Photo \[one\]](https://example.com/photo.png)

即:字面方括号被反斜杠转义为\[/\],保证它作为纯文本存在于 alt 中,而不会在 Markdown 渲染阶段被误解析为链接语法。

转义发生的位置:Turndown 图片规则的 alt 处理

defuddle 的 Markdown 输出由 src/markdown.ts 中的createMarkdownContent完成,其底层基于 TurndownService。图片的转换由两个自定义规则实现。

普通<img>规则

// src/markdown.ts 第 447-456 行(image 规则) turndownService.addRule('image', { filter: 'img', replacement: function(content, node) { if (!isGenericElement(node)) return content; const alt = escapeInlineText(node.getAttribute('alt') || ''); const src = formatMarkdownLinkDestination(getBestImageSrc(node)); const titlePart = formatMarkdownLinkTitle(node.getAttribute('title')); return src ? `${alt}` : ''; } });

<figure>规则

// src/markdown.ts 第 404-444 行(figure 规则) const alt = escapeInlineText(img.getAttribute('alt') || ''); const src = formatMarkdownLinkDestination(getBestImageSrc(img)); ... return `${src ? `${alt}` : alt}\n\n${caption}\n\n`;

两条规则对 alt 文本都调用了同一个转义函数escapeInlineText,这正是Photo [one]变成Photo \[one\]的关键所在:

// src/markdown.ts 第 204-205 行 const escapeInlineText = (value: string): string => escapeEntityLike(baseEscape(value.replace(/[\r\n]+/g, ' ')).replace(/</g, '\\<'));

它的工作顺序是:

  1. 折叠换行:[\r\n]+统一替换为单个空格,防止 alt 中的换行破坏 Markdown 行结构;
  2. Turndown 基础转义:baseEscape会按 Markdown 语法规则转义[]()、\等保留字符——[与]在这里被转义为\[与\];
  3. 标签序列防护:<被强制转义为\<,避免 alt 中的<tag>形态内容在 CommonMark 渲染器(如 Obsidian)中被当作原始 HTML 吞掉(对应 issue #285 的同类问题);
  4. 实体形态再转义:escapeEntityLike(src/markdown.ts 第 65-67 行)会把形如&#60;、&lt;这类实体形态中的&再转义为&amp;,防止二次解码时被还原成可执行 HTML。

因此 fixture 中的alt="Photo [one]"会得到Photo \[one\],最终输出![Photo \[one\]](https://example.com/photo.png)。

目标 URL 的净化:危险协议过滤

alt 只负责"显示文本",图片的src才是真正决定输出安全性的部分。formatMarkdownLinkDestination(src/markdown.ts 第 69-76 行)在拼接[![alt](https://gitcode.com/gh_mirrors/de/defuddle/blob/f6ca46d6570b74367813f727b854b94f7ff5a449/src?utm_source=gitcode_repo_files)](https://link.gitcode.com/i/287975b7b1e2e94c4649fa67792cf506)前执行了多层净化:

function formatMarkdownLinkDestination(href: string): string { if (isDangerousUrl(href)) return ''; href = escapeEntityLike(href) .replace(/\\/g, '\\\\') .replace(/[<>\u0000-\u001f\u007f]/g, char => encodeURIComponent(char)); if (!/\s/.test(href)) return href.replace(/([()])/g, '\\$1'); return `<${href}>`; }
  • 危险协议直接拒绝:isDangerousUrl(src/utils/dom.ts 第 73-80 行)会剥离空白与控制字符后检查协议。javascript:、blob:一律视为危险;data:仅当是data:image/内联图片时才放行,其余全部拒绝——因为 data/blob URL 可以把整个文档塞进属性里,绕过脚本与事件属性剥离;
  • 结构字符转义:<、>、控制字符被encodeURIComponent编码,空格存在的 URL 用<...>尖括号形式包裹,URL 中的括号()会被\(转义,避免与 Markdown 目标语法冲突;
  • 空目标退化为纯文本:当src被判定为危险 URL 时返回空串,image规则此时不输出图片语法而是直接降级;figure规则则只保留alt文本。

前后端双重验证:fixture 与 XSS 测试

fixture 对比测试

security--markdown-image-escaping.md 属于 tests/expected/ 目录下的期望输出,格式为"JSON 元数据代码块 + Markdown 内容"。它由 tests/fixtures.test.ts 驱动:测试自动发现 tests/fixtures/ 下的全部.html文件,用Defuddle(doc, url, { separateMarkdown: true, fetch: offlineFetch })处理(离线 fetch 保证提取路径确定、可复现),再把结果与 expected 文件逐字节比较。新增 fixture 只需放入.html文件后运行npm test生成基线,再人工审查 expected 输出。

Markdown 输出安全测试

tests/markdown-xss.test.ts 对 Markdown 输出做了更强的"再渲染"验证。其核心辅助函数expectSafe会把 Markdown 用marked解析回 HTML,然后断言:

  • 所有元素没有任何on*事件属性;
  • 所有href/src都通过isDangerousUrl检查;
  • 页面中不存在script或带srcdoc的iframe。

针对图片 alt 与标题,该文件专门有一组测试覆盖了"字面方括号 + 尖括号 + 反斜杠 + 实体 + 特殊字符"的组合:

test('preserves image labels, titles, and structural URL characters without adding elements', () => { const alt = 'A [label] <tag> and \\ brackets &lt;b&gt; &copy; A & B'; const title = 'A "title" with \\ and <tag>'; const src = 'https://example.com/p(1).png?x=1&y=2'; const rendered = expectSafe(createMarkdownContent(`<img alt="${escapeHtml(alt)}" title="${escapeHtml(title)}" src="${escapeHtml(src)}">`, url)); expect(rendered.querySelectorAll('img')).toHaveLength(1); expect(img.getAttribute('alt')).toBe(alt); expect(img.getAttribute('title')).toBe(title); expect(img.getAttribute('src')).toBe(src); });

该测试确认了三点关键行为:alt 中的方括号等字符经过转义后,渲染回 HTML 时alt属性值与原始文本逐字一致(说明转义没有破坏文本语义);src中带括号的查询参数 URL 在"Markdown 转义 → HTML 还原"的往返中保持同一目标;并且整个过程不会产生额外元素。这与 fixture 中"description should remain plain image text"的预期完全对应。

另外该文件还验证了 srcset 选择的安全边界:当srcset中混入javascript:alert(1)等危险候选时,getBestImageSrc(src/markdown.ts 第 136-169 行)只会采纳通过isDangerousUrl检查的最大宽度候选,测试断言最终输出回退到安全的src。

端到端复现:用 CLI 与 Node API 观察转义结果

你可以在本地复现这一转义行为。仓库 README 提供了三种入口(README.md 第 19-148 行)。

CLI 方式

# 解析本地 HTML 文件并输出 Markdown npx defuddle parse tests/fixtures/security--markdown-image-escaping.html --markdown

--markdown(别名-m)会开启 Markdown 转换(README 的 CLI 选项表见 README.md 第 102-116 行)。对上述 fixture 运行后,图片行应输出为![Photo \[one\]](https://example.com/photo.png)。

Node.js 方式

import { parseHTML } from 'linkedom'; import { Defuddle } from 'defuddle/node'; const { document } = parseHTML(htmlWithImage); const result = await Defuddle(document, 'https://example.com/article', { markdown: true }); console.log(result.content); // 包含 ![Photo \[one\]](https://example.com/photo.png)

对应选项:markdown(默认false,把提取内容转换为 Markdown)与separateMarkdown(默认false,保留 HTML 到content、Markdown 到contentMarkdown),二者最终都经由toMarkdown调用createMarkdownContent(src/markdown.ts 第 1014-1024 行)。fixture 测试采用的正是separateMarkdown: true路径。

更深一层:从源代码结构看转义管线的设计取舍

从 src/markdown.ts 的结构可以推断,defuddle 的 Markdown 转义不是逐条硬编码规则,而是一条贯穿全部文本出口的公共管线:

  • 所有文本出口统一走转义:除图片 alt 外,链接文本、callout 标题(escapeInlineText)、图片 title(formatMarkdownLinkTitle)等同样复用同一套转义逻辑,保证[、]、<、&等字符在任何上下文中都以相同规则处理;
  • 转义发生在 DOM 层面之外:HTML 提取阶段先由 src/standardize.ts 与 src/elements/images.ts 完成图片去重、懒加载data-src/data-original提升、srcset归一化等标准化(alt属性在白名单 ALLOWED_ATTRIBUTES 中,会被保留传递),Markdown 转义只负责"最后一步"的语法化,职责单一;
  • 转义以渲染器兼容为目标:escape覆写(src/markdown.ts 第 193-203 行)特意只转义"后接标签形态字符"的<,从而保留<a@b.com>、<https://x>这类合法 autolink 不被破坏,可见转义策略是"最小必要转义"。

结语

security--markdown-image-escaping这条 fixture 虽短,却浓缩了 defuddle Markdown 输出层的两个核心目标:语法正确(alt 中的字面方括号必须转义为\[one\],否则会被渲染成链接)与输出安全(目标 URL 必须经过isDangerousUrl过滤,javascript:/blob:/非图片data:一律拒绝)。理解 src/markdown.ts 中escapeInlineText与formatMarkdownLinkDestination的分工,以及 tests/markdown-xss.test.ts 的"再渲染断言"验证方式,可以帮助你在接入 defuddle 时判断自己的 Markdown 管道是否也需要同等级别的转义与净化防线。

  • 后端

【免费下载链接】defuddle

Get the main content of any page as Markdown.

项目地址:https://gitcode.com/gh_mirrors/de/defuddle
点击查看免费下载
上一篇:PyO3 教程:使用 `[pyclass]` 与 `[pymethods]` 在 Rust 中定义 Python 类
下一篇:opensource.guide 开源贡献实战指南:从选择项目、提交 Pull Request 到融入社区的全流程解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:55:40

重卡变身“移动储能”入网、V2G计量标准落地、金冠签19.8亿出海大单:补能进入“重卡入网+全球出海”时代

浙江首次开展新能源重卡与电网互动响应&#xff0c;重卡充电站接入虚拟电厂、参与市场化填谷&#xff0c;最大响应负荷4.76万千瓦&#xff1b;金华10余座重卡充电站集体参与电力市场化响应&#xff1b;国家发改委"十五五"规划提出虚拟电厂最大调节能力超5000万千瓦&a…

作者头像 李华
网站建设 2026/10/15 11:54:55

Docker搭建Prometheus+Grafana监控系统实战指南

1. 这不是“装个监控”那么简单&#xff1a;为什么PrometheusGrafana组合在今天依然值得你亲手搭一遍你可能已经见过太多“一键部署监控平台”的宣传&#xff0c;点几下鼠标、复制粘贴几行命令&#xff0c;界面就亮了——CPU曲线跳动&#xff0c;内存柱状图起伏&#xff0c;甚至…

作者头像 李华
网站建设 2026/10/15 11:54:47

高级路由实验:OSPF与EIGRP双点双向重分发及备份链路实战

1. 实验背景与整体设计思路1.1 为什么高级路由实验是网络学习的分水岭做过几个基础实验之后&#xff0c;很多人会有一种错觉&#xff1a;配个IP、起个路由协议、ping通就完事了。但真正到了高级路由实验这个环节&#xff0c;你会发现事情没那么简单。基础实验里&#xff0c;网络…

作者头像 李华
网站建设 2026/10/15 11:54:46

C++实验6报告:类与对象、继承多态实操指南

写C实验报告这事&#xff0c;说简单也简单&#xff0c;说难也难。我见过太多人到了实验6还是把报告写成"代码粘贴运行截图"&#xff0c;结果代码能跑&#xff0c;分数却上不去。实际上实验报告的价值不在于记录你做了什么&#xff0c;而在于证明你理解了为什么这么做…

作者头像 李华