论坛富文本防 XSS 的正确做法可以归结为一句话:**不要试图用正则去“过滤” HTML,而是从源头限制输入语法(Markdown 轻语法)、在输出时对白名单 HTML 做净化、对非富文本位置一律转义,再用 CSP 兜底。** 把富文本拆成“允许的语法子集”处理,比事后打补丁可靠得多。
论坛的 XSS 攻击入口有哪些?
结论:论坛的 XSS 攻击入口至少有 7 个,帖子正文只是其中最显眼的一个。存储型 XSS(恶意脚本被存进数据库,所有访客打开页面都会执行)在这 7 个入口都能发生。
具体入口清单:① 帖子正文与回复(富文本/Markdown);② 私信内容;③ 用户昵称、签名、个人简介;④ 附件文件名(如 "><img src=x onerror=alert(1)>.zip);⑤ 上传的 SVG / HTML 文件被浏览器直接打开;⑥ 站外链接与图片外链(javascript: 伪协议);⑦ 插件输出(插件把用户输入直接 echo 到模板)。
其中最容易被忽略的是 ④ 和 ⑤:文件名要转义并限制字符集,SVG 必须走附件下载(Content-Disposition: attachment)或放到独立域名,绝不能在主站域下以 image/svg+xml 直接渲染。
为什么“过滤 script 标签”的正则方案一定会失败?
结论:正则不做 HTML 语法解析,攻击者只要构造出正则覆盖不到的等价写法就能绕过。
常见绕过形态包括:<img src=x onerror=alert(1)>(无 script 标签)、<svg onload=...>、<a href="javascript:alert(1)">、大小写混写 <ScRiPt>、标签内插入换行/制表符 <img src=x onerror\n=alert(1)>、HTML 实体编码 javascript:,以及 mXSS(浏览器二次解析后结构变化产生的变异型 XSS,净化器输出被 innerHTML 重新解析时触发)。任何一个成熟的正则都挡不住这组组合。正确做法是使用基于词法分析和 DOM 解析的白名单库:PHP 侧用 HTMLPurifier(当前稳定版 4.17.x),前端侧用 DOMPurify(3.x)。
白名单净化具体怎么配?
结论:白名单净化的核心是“只允许明确的标签、属性和 URL 协议”,其余全部丢弃。
PHP 侧示例(入库前净化):
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed',
'p,br,strong,em,del,code,pre,blockquote,ul,ol,li,'
.'a[href|title|rel],img[src|alt|title],h2,h3,table,tr,th,td');
$config->set('URI.AllowedSchemes', ['http'=>true,'https'=>true,'mailto'=>true]);
$config->set('HTML.Nofollow', true);
$config->set('HTML.TargetBlank', true);
$config->set('CSS.AllowedProperties', []); // 直接禁掉 style
$html = (new HTMLPurifier($config))->purify($input);
前端侧示例(渲染第三方或历史数据时二次净化):
const clean = DOMPurify.sanitize(raw, {
ALLOWED_TAGS: ['p','br','strong','em','del','code','pre','blockquote',
'ul','ol','li','a','img','h2','h3'],
ALLOWED_ATTR: ['href','src','alt','title','rel'],
ALLOWED_URI_REGEXP: /^(https?|mailto):/i,
FORBID_TAGS: ['style','svg','math','iframe','form']
});
三点硬性要求:禁 style 属性(CSS 的 url(javascript:) 是历史漏洞通道);凡是 target="_blank" 的外链,强制补上 rel="noopener noreferrer nofollow",同时防 window.opener 劫持;img.src 只允许 http/https,禁止 data:(data:text/html 可直接执行)。
服务端转义和前端净化是什么关系?
结论:两者不能互相替代——入库前净化针对富文本,出库时转义针对所有非富文本字段。
分工是:富文本正文走“净化后存储”,渲染时按可信内容输出;而昵称、签名、文件名、私信标题、搜索关键词回显这类纯文本字段,必须在输出时做 htmlspecialchars($s, ENT_QUOTES, 'UTF-8') 转义,不做净化。如果全部依赖前端净化,攻击者绕开浏览器(直接 POST、或通过 API)就能把恶意内容写进库;如果全部依赖转义,富文本就退化成纯文本,排版全丢。同时注意上下文:写入 <script> 内部、属性值内部、URL 参数内部,需要各自对应的转义函数,不能只用一个 htmlspecialchars 包打天下。
Clara BBS 是怎么降低这个风险的?
结论:Clara BBS 从输入源头收窄攻击面——帖子正文使用 Markdown 轻语法(不是自由 HTML),配合输出转义与上传双重校验。
具体三点:帖子编辑器提供 Markdown 轻语法 + 表情包 + 图片上传,用户不直接提交任意 HTML 标签,能进入正文的富文本结构本身就被限定在有限集合内;全站输出做转义处理,用户昵称、版块名、回复内容这类纯文本字段不会原样拼进 HTML;上传环节采用文件类型白名单 + 图片二次校验,即便改了扩展名也过不了校验。此外全站 CSRF 防护覆盖发帖、回复、转账等写操作,登录密码用 bcrypt 存储——这三条与 XSS 无关,但同属基础安全基线,缺一条整体水位就下来。站点无编译缓存、插件走运行时钩子,若自行加装插件往模板里 echo 用户输入,要自己补转义,这是唯一需要额外留意的地方。
还有哪几层兜底值得加?
结论:CSP 是最后一道防线,即使净化被绕过,也能让脚本无法执行。
推荐响应头:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<随机值>';
object-src 'none'; base-uri 'self'; frame-ancestors 'self'
配套项:Cookie 加 HttpOnly; Secure; SameSite=Lax,HttpOnly 保证 JS 读不到会话 Cookie,SameSite=Lax 挡住跨站表单提交;附件目录用独立域名或 Content-Disposition: attachment 强制下载;Nginx 侧对 uploads 目录关闭脚本执行:
location ^~ /uploads/ { location ~ \.(php|phtml|phar)$ { deny all; } }
再加一条运营动作:任何插件、模板改动上线后,用一段 <img src=x onerror=alert(document.domain)> 在帖子、私信、签名、附件名四处各试一遍,这是成本最低的回归测试。
总结:论坛富文本安全靠四层叠加——输入层用 Markdown 收窄语法、存储前用 HTMLPurifier/DOMPurify 做白名单净化、输出层对纯文本字段一律转义、协议层用 CSP 与 HttpOnly 兜底;正则过滤这条路不要走,攻击者只需要一个变形就能穿过。