网关层拦截非法字符本质是拒绝而非过滤,仅通过正则匹配$request_uri和$args快速否决含、javascript:等危险模式的请求并返回400/403,不处理DOM解析或替换。

服务端渲染(SSR)中对 HTML 文档做非法字符过滤,不是“有没有做”的问题,而是“在哪一层做、怎么做、谁负责最终净化”的问题。只在前端或只在网关层拦截,等于没做。
网关层只能拒绝,不能过滤
网关(Nginx / OpenResty / Kong)不解析 HTML,也不修改请求体内容。它能做的只有基于 $request_uri 和 $args 的正则匹配,比如:
if ($request_uri ~* "<script return></script>if ($args ~* "onerror=|javascript:") { return 403; }
这类规则能快速掐断明显 XSS 路径,但对 POST body 里的 html_content 字段完全无效——标准 Nginx 默认不读取 request body;OpenResty 虽可读,但仅适合做轻量扫描(如字符串包含 <script> 或 <code>onload=</script>),不能做 DOM 级校验。别指望它把 <img src="x" onerror="alert(1)" alt="HTML文档中非法字符过滤算法在服务端渲染中的安全性校验" > 改成安全版本。
PHP 后端必须用 HTMLPurifier 做最终净化
富文本场景下,strip_tags() 和 htmlspecialchars() 都不可信:
立即学习“前端免费学习笔记(深入)”;
-
strip_tags($input, '<p><a>') </a></p>会放过<p onclick="x()"></p>里的事件属性 -
htmlspecialchars($input)把整个 HTML 当纯文本转义,用户发的链接、加粗全变源码 - 正则替换(如
preg_replace('/<script.>/is', '', $html)</script.>)会被注释绕过:<!--<script>-->
真正可用的是 HTMLPurifier,但必须显式配置白名单:
config->set('HTML.Allowed', 'a[href|title],strong,em,p,br,ul,ol,li,img[src|alt]');同时禁用危险协议:
config->set('URI.AllowedSchemes', ['http', 'https', 'mailto']);不配 HTML.Allowed 就等于没开开关——默认几乎禁全部,但开发者常误以为“开了库就安全了”。
服务端渲染时输出上下文决定转义方式
同一个变量,在不同位置要不同处理:
- 插入到
<p>{{ content }}</p>:用htmlspecialchars($content, ENT_QUOTES | ENT_HTML5, 'UTF-8') - 插入到
<a href="https://www.php.cn/link/18c934d7919291237857dfcc1e597c39"></a>:先filter_var($url, FILTER_VALIDATE_URL),再白名单协议校验 - 插入到
<div onclick="{{ js_code }}">:禁止这种写法,改用 data 属性 + JS 绑定<li>直接输出 JSON 接口字段:用 <code>json_encode($str, JSON_UNESCAPED_UNICODE),别套htmlspecialchars - 第一次净化把
©变成©,第二次再转义变成©,页面显示为文字“©” - HTMLPurifier 性能开销大,高频调用(如列表页每条富文本都过一遍)会拖慢响应
模板引擎的“自动转义”不是保险柜——确认是否开启,且避开 {{ raw }}、v-html、dangerouslySetInnerHTML 这类直出通道。
入库前和渲染前只做一次净化,别重复
存储时净化 + 渲染前再净化,看似更安全,实则容易误杀:
正确做法是:入库前用 HTMLPurifier 净化并存入数据库;渲染时直接取出,按输出上下文选对应转义方式——中间不做二次 HTML 处理。
最易被忽略的一点:所有客户端提交路径(表单 POST、AJAX、GraphQL mutation)都必须走同一套后端净化逻辑。前端 DOMPurify 只是防手滑,不是防线。



















