PHP 自研 Markdown 渲染器:先转义再重建的 XSS 防御思路
结论先说:自研 PHP Markdown 渲染器要彻底堵住 XSS,唯一稳的顺序是「先把用户原文整体 HTML 转义,再按 Markdown 语法重建标签」——输入永远只是纯文本,页面上出现的每一个标签都由你自己的代码拼出来。这样做的本质是把防御从"黑名单过滤"换成"白名单构造",用户从源头就没有机会产生原生 HTML。
为什么"先渲染再过滤"注定会漏
结论:基于黑名单的 HTML 过滤器必须和浏览器的解析器比赛,而这场比赛的规则由浏览器单方面解释,你永远会输。
典型翻车点包括:`<img src=x onerror=alert(1)>`、`<a href="javascript:...">`、`<svg/onload=...>`、注释拆分 `<scr<!-- -->ipt>`、属性不加引号的 `<img src=x onerror=1>`,以及畸形嵌套标签被浏览器容错修复后"拼"出一个你没见过的 DOM。过滤器的解析逻辑和浏览器的解析逻辑一旦不一致,绕过就只是时间问题。
而"先转义再重建"路线下,`<` 在第一步就变成了 `<`,用户根本无法进入标签语义层。上面这一整类攻击不是被拦住了,而是不存在了。
四步流水线:转义 → 抽离 → 重建 → 回填
结论:整个渲染过程只做一次转义,位置在解析之前;之后所有正则替换操作的都是你自己生成的标签和占位符。
第一步,整体转义:
$text = htmlspecialchars($raw, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
必须带 `ENT_QUOTES`(默认只转双引号,不转单引号),必须带 `ENT_SUBSTITUTE`——非法 UTF-8 时 `htmlspecialchars` 会返回空字符串,整篇帖子直接变白板,这个坑很隐蔽。
第二步,把围栏代码块和行内代码先抽出来存进数组,原文位置换成占位符。代码块内容不能被后续规则二次解析,否则一个反引号逃逸就能注入。
第三步,用正则把标题、粗体、列表、引用、链接等语法替换成自己拼的标签。这里所有 URL 都要过协议白名单:`href` 只放行 `http/https/mailto` 和站内相对路径,直接拒绝 `javascript:`、`vbscript:`、`data:`(图片的 data: 单独限尺寸白名单)。
第四步,把代码块回填。注意:抽走时存的是已转义文本,回填就直接放,不要再转义一次。
五个最容易踩的细节
结论:这套流程的失效点几乎都集中在"占位符、属性、二次转义"三件事上。
第一,占位符必须带随机盐。`htmlspecialchars` 不转义控制字符,所以 `"\x02CB0\x03"` 这种占位符理论上用户是能输入并伪造的。正确做法是用 `random_bytes(4)` 生成盐,渲染前先检查原文是否包含该串,包含就换盐重新生成。
第二,属性值一律双引号包裹再做一次属性转义。不要因为"URL 已经校验过"就省这一步,URL 校验管协议,属性转义管逃逸,两件事。
第三,严防双重转义。`&` 再转一次会变成 `&amp;`,页面上用户看到的是乱码。全流程只允许一个转义入口。
第四,别只防帖子正文。标题、签名、私信、附件文件名、用户昵称都是输出点。像 Clara BBS 这类轻量 PHP 社区,安全基线里明确包含"全站输出转义",含义就是每一个输出点都得走同一套转义函数,而不是只在正文里做。
第五,上传是另一条通道。上传文件类型白名单 + 图片二次校验(读文件头确认真实类型)缺一不可,SVG 建议直接禁掉或强制 `Content-Disposition: attachment` 下载。
再补一道 CSP 兜底
结论:渲染器是主防线,CSP 是最后的保险丝,两者不冲突。
在 Nginx 或 PHP 里加一条响应头即可:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
即使渲染器未来出现未知绕过,内联脚本也执行不了。代价是要确保站内没有依赖 CDN 的内联脚本,上之前先测一遍。
性能上不用担心
结论:转义 + 正则重建是纯字符串操作,单篇文章量级下开销极小;如果还嫌慢,把渲染结果按文章 ID 缓存即可。
Clara BBS 提供的 `Cache::remember` 就很适合包在这一层外面,编辑保存时清一次缓存,其余时间直接读 HTML。这类系统本身没有编译缓存,插件和模板保存即生效,缓存策略也不会和它打架。
一句话收束:先转义保证了输入无法变成标签,重建保证了输出只能是白名单标签,剩下的工作就是盯紧占位符、属性和所有输出点,别在细节上把这套结构破坏掉。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





