跳到主要内容
我的站点
首页 归档 分类 标签 案例 服务 关于 聊聊需求

Markdown 渲染的 XSS 边界:自研渲染器踩过的四个坑

内容站的 XSS 几乎都从 Markdown 渲染进来。自研渲染器要处理的不只是转义,还有链接协议、图片事件、原始 HTML 与属性注入——四个坑与对应写法。

Markdown 渲染的 XSS 边界:自研渲染器踩过的四个坑 封面

#背景

内容站的攻击面很集中:所有用户可控的内容,最终都会经过 Markdown 渲染输出成 HTML。后台只有管理员能写文章,看起来风险不高,但评论、询价留言、AI 生成的内容都会流进同一套渲染逻辑;而"只有管理员能写"这个前提,在多人协作或多角色后台里随时会变。

所以渲染器必须按"输入不可信"来写。以下是实际踩过并修掉的四个坑。

#四个坑

#1. 只转义了尖括号,漏了链接协议

php
// 错误:链接地址直接输出
$html = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2">$1</a>', $text);
// 输入 [点这里](javascript:alert(document.cookie)) 就中招

正确做法是协议白名单,不在名单里的一律降级为纯文本(不要只做字符串替换,java\nscript: 这类变体绕得过去):

php
$href = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
if ($scheme !== '' && !in_array($scheme, ['http', 'https', 'mailto'], true)) {
    return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');   // 不允许的协议:只输出文字
}

相对路径(/posts/xxx)的 parse_url 取不到 scheme,属于合法情况,所以要允许 scheme === ''。

#2. 图片的 onerror 与 srcset

![x](a.png" onerror="alert(1)) 这种注入靠的是属性值没有转义引号。转义要覆盖 "、'、<、>、& 五个字符(ENT_QUOTES),并且自己拼接属性时不要再手工加引号:

php
$src = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$alt = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
return '<img src="' . $src . '" alt="' . $alt . '" loading="lazy" decoding="async">';

#3. 原始 HTML 直通

很多渲染器允许 Markdown 里嵌 HTML(方便加 <div>、<iframe>)。一旦直通,前面所有转义都白做——<img src=x onerror=...> 直接过。

取舍很清楚:要么关掉原始 HTML,要么对允许的标签做白名单清洗。本项目选前者:正文不支持原始 HTML,需要嵌入时用受控语法(视频用平台组件、代码用围栏块)。少一个功能,少一整类漏洞。

#4. 代码块里的内容被当标签解析

代码块内的 <script> 必须原样展示,因此渲染顺序很关键:先把代码块抽出来占位,其余部分转义,最后再把代码块内容以转义后的形式放回。顺序颠倒就会出现"代码高亮把标签注入了"这种低级但常见的漏洞。

#验证方法

渲染器的安全测试不能靠肉眼看页面,要写成断言:

php
foreach ([
    '[x](javascript:alert(1))',
    '![x](a.png" onerror="alert(1))',
    '<img src=x onerror=alert(1)>',
    '[x](data:text/html;base64,PHNjcmlwdD4=)',
] as $payload) {
    $html = MarkdownService::render($payload);
    assert(!str_contains($html, 'onerror'));
    assert(!str_contains($html, 'javascript:'));
}

再加一层兜底:全站 CSP 里 script-src 保持 'self'(不写 unsafe-inline),即使渲染器漏了一处,内联脚本也执行不了。

#取舍

  • 不支持原始 HTML:少了排版自由度,换来可证明的边界——这是内容站该做的选择。
  • 图片地址不做代理:外链图片直出会有隐私与失效问题,但代理会引入带宽与存储成本;本站的做法是后台配图一律走素材库(本地路径),正文外链只做协议校验。
  • CSP 不是替代品:它拦的是"执行",拦不住"内容被篡改"。两层都要有。
最后更新: 本文采用 CC BY-NC-SA 4.0 许可,转载请注明出处与作者。
related

相关文章

评论

还没有评论,来说点什么吧。

发表评论

评论需站长审核后展示 · 最多 1000 字