富文本字段服务端验证与XSS清洗实战指南
1. 为什么富文本字段的验证这么容易出问题先问个直白的问题你在表单里写验证脑子里的第一反应是什么非空、长度、格式。这没错但一旦字段类型变成富文本这三板斧就全都不够用了。富文本和别人不一样的地方在于它天生就自带一套HTML。用户在里面写的不是hello而是这段p stylecolor: red;hello bworld/b/p这套HTML是展示层的核心价值——加粗、变色、超链接、列表全靠它。但问题也出在这里只要允许用户传入HTML就等于把一部分代码执行权交了出去。如果只做非空和长度校验你收到的可能是这种内容scriptalert(xss)/script那这个验证就名存实亡了。因为你亲手把一个可执行脚本存进了数据库再吐回给全站用户。后端那些安全意识不够强的接口往往就是这么被骗出脏数据的。所以在正经项目里富文本字段的验证从来不是一道是否为空的判断题而是一道双向的安检门内容是否真有业务价值——过滤掉只有标签、没有实质内容的空富文本内容是否安全可存储——过滤掉能执行任意脚本的恶意HTML。这篇不打算贴一个现成的库让你抄完走人而是完整走一遍我的思考路径先弄明白服务端验证到底管什么再摸清清洗脚本的常规实现接着把那些容易漏掉的边界情况逐个踩一遍最后聊聊验证不过时该拒绝还是该清洗。这样你自己动手写的时候才能知道每行代码是干什么的。2. 先把验证拆成三件事必填、长度、格式清洗富文本字段的验证我习惯在服务端拆成三道关卡。前端也有校验但那只是给正常用户看的提示牌不能被当成安全边界。真正不能被绕过的校验必须发生在数据进入服务器之后。2.1 必填校验不是非空是有实际内容普通文本字段写个空字符串判断就完事富文本不行。你想想用户在编辑器的工具栏上瞎点了一通插入了一张图片又删掉最后内容区域里只剩这么点东西pbr/p字符串长度不为零但用户其实什么都没写。如果你按非空即合法来处理存进数据库的就是一堆废标签页面渲染出来是一片空白不仔细看网页源码你都不知道问题出在哪。我的判断标准是先把HTML转成纯文本去掉所有标签之后再trim掉空白字符。剩下的内容长度必须大于某个阈值才算是有实际内容。// 以 Go 为例把 HTML 转纯文本再判断 func isBlankRichText(htmlContent string) bool { text : stripTags(htmlContent) // 去掉所有标签 if len(strings.TrimSpace(text)) 0 { return true } return false }2.2 长度校验按纯文本而不是按标签算富文本的长度校验也是个容易翻车的地方。用户加粗几个字、换个字体颜色标签就一大坨span stylecolor:#FF0000; font-size: 18px;一段内容/span如果按字符串长度统计这段没多少字的内容轻松就能撑爆你设的长度上限。所以长度校验要分两层第一层纯文本长度——用户实际看到了多少个字这才是有意义的长度第二层HTML源码长度——防止有人往HTML标签的属性里塞大量参数导致整段数据臃肿。我在项目里通常给纯文本设为一个符合业务值的上限比如5000字给原始HTML设置为纯文本上限的2~3倍专门防这种迂回攻击。2.3 格式清洗这是验证的核心战场第三关才是重头戏。所谓格式清洗不是简单判断里面有没有script标签而是把HTML解析出来逐个节点过一遍标签名是否在白名单里属性是否在白名单里属性的值是否合法比如href不能以javascript:开头。这一步做扎实了XSS脚本、危险的协议、非法嵌套基本都能挡住。等到了后文标签白名单属性白名单的环节我会专门展示我自己的那套过滤列表。3. 服务端清洗流程与代码示例工具选型上其实不用自己从零手写一个HTML解析器。目前主流的做法都是拿着成熟的解析库解析成DOM树然后遍历节点做可留则留、该删则删的判断最后重新序列化成HTML。我拿Go语言举个例子用的库是golang.org/x/net/htmlGolang官方的扩展包稳定性比较有保障。3.1 处理流程总览我的处理流程分五步走步骤具体操作目的1解析HTML字符串为DOM树让每个标签、属性都能被单独审查2遍历所有节点逐个判断标签名不在白名单的标签直接移除内容3对保留的标签检查属性只留下安全属性其他统统删除4对特定属性做值校验拦截伪造的协议头5重新序列化为HTML字符串输出可存储的干净HTML3.2 标签白名单怎么定白名单的思路很简单在富文本编辑器里用户能用到的无非是排版、加粗、斜体、超链接、图片这些基本功能。那白名单就只保留这些标签其他的一律不放行。var allowedTags map[string]bool{ p: true, br: true, b: true, strong: true, i: true, em: true, u: true, strike: true, a: true, img: true, ul: true, ol: true, li: true, blockquote: true, pre: true, code: true, h2: true, h3: true, h4: true, }注意一个细节h1标签我默认不放行。原因不是技术上的而是内容排版上的。富文本内容通常嵌在页面里页面上已经有自己的H1标题了如果正文里再混入一个H1整页的标题层级会乱掉。这种属于实际业务里走出来的坑每个团队的约定可以不一样但一定要有约定。3.3 属性白名单与值校验标签过关了属性也得过一道筛子。比如a标签如果你不管它的href人家就能夹带私货a hrefjavascript:alert(1)点击/a这就是经典的XSS攻击手法点击链接直接执行脚本。我的做法是给每个标签单独声明它允许携带的属性示例里写成白名单结构再单独加一层属性值校验。var allowedAttrs map[string]map[string]bool{ a: {href: true, title: true}, img: {src: true, alt: true, width: true, height: true}, // 其他标签若没声明属性一律视为无属性合法 } func sanitizeAttrs(n *html.Node) { if n.Type ! html.ElementNode { return } tag : n.Data var kept []html.Attribute for _, attr : range n.Attr { // 1. 判断属性是否在该标签允许的范围内 if !allowedAttrs[tag][attr.Key] { continue } // 2. 对URL类型的属性强制校验协议头 if attr.Key href || attr.Key src { if !isSafeURL(attr.Val) { continue } } kept append(kept, attr) } n.Attr kept } func isSafeURL(raw string) bool { raw strings.TrimSpace(strings.ToLower(raw)) if strings.HasPrefix(raw, javascript:) { return false } if strings.HasPrefix(raw, data:text/html) { return false } // 允许 http / https / 相对路径 / 纯锚点 if strings.HasPrefix(raw, http://) || strings.HasPrefix(raw, https://) || strings.HasPrefix(raw, /) || strings.HasPrefix(raw, #) || strings.HasPrefix(raw, mailto:) { return true } return false }你可能会问直接在属性白名单里不让href出现不就行了不行因为超链接是富文本的基本功能你把它禁了用户的排版需求就废了一半。所以正确的姿势是允许它但盯着它的值。3.4 用正则做补充校验虽然解析器是主力但正则也不是不能用。我一般拿正则做辅助校验而不是主过滤逻辑。原因很简单HTML的嵌套结构太复杂正则没法可靠地解析DOM树。比如下面这个标签叠加写法用正则匹配script基本配不干净scrscriptiptalert(1)/script真正拿正则干活的场景是数据入库前的最后一道兜底检查比如全局搜一遍onerror、onload这类事件属性名如果解析器因为某些边界情况漏过了这层兜底能把最后漏网的干掉。注意正则兜底永远有极限不要试图用正则来解决全部HTML清洗问题。正确定位是辅助和最后一道主filter必须靠HTML解析器。4. 必须人工盯防的边界情况上面那套流程能解决80%的常规问题但剩下20%是藏在犄角旮旯的边界情况。这些情况不处理测试环境能过上了生产就出事故。4.1 空白字符与零宽字符用户从Word或者其他编辑器复制内容时最容易带进来一堆不可见字符不断行空格(\u00A0)、零宽空格(\u200B)、全角空格。这些字符在页面上看不见但会让你的纯文本长度校验结果虚高入库的数据也带着一股怪味儿。清洗方案在转纯文本时把所有Unicode空白类字符统一规约成半角空格再做trim处理。4.2 图片标签的src属性我见过不少团队在属性白名单里直接放行img标签的src理由是就一个图片地址能出什么事。但你要知道src如果被篡改成data:image/svgxml;base64,...这种格式SVG文件里是可以内嵌脚本的。这就是SVG XSS攻击。所以我在isSafeURL里特意把data:text/html拦掉了但data:image这类以data:开头的协议也需要单独评估。我的经验是如果业务上确实允许base64图片那要求服务端有独立的图片上传接口禁止把大段base64内容存在富文本字段里。不然数据臃肿只是一方面隐患才是要命的。4.3 标签嵌套错乱还有一种情况用户手动改HTML源码或者从别的系统直接搬运标签没闭合就贴进来了pb加粗文字p新段落/b/p这种HTML虽然不标准但浏览器会自动容错渲染出来可能就是加粗段落到第二行还在加粗。你清洗的时候不能抱着用户输入不合法就直接报错的心态因为前端编辑器可能已经帮你把内容改得七七八八了。清洗逻辑应该是解析器解析不出来就丢弃解析结果为nil就整段拒绝而不是搞模糊匹配。宁可不存也不能存进去一段不可预知的HTML。4.4 事件属性你漏掉的合法属性再提醒一个容易被忽略的点你在标签白名单里放行了p标签但p标签的onmouseover属性呢p onmouseoveralert(document.cookie)内容/p解析器会巴不得让你把onmouseover留在属性列表里——如果你没做属性白名单的话任何标签上的任何事件监听属性都会原样保留。这类属性在XSS攻击里简直是重灾区onload、onerror、onclick、onmouseover、onfocus不管标签名是什么这些事件属性只要出现一律不得入内。这也是为什么我强烈建议在属性白名单里使用允许清单而不是禁用清单。禁用的思路永远是滞后的因为新的攻击方式层出不穷而允许的思路是收缩的除非有业务需要否则一律不放行。5. 验证不通过时拒绝还是清洗写服务端验证的时候很多人会陷入一个纠结用户提交的内容不合规我们到底应该直接返回错误让用户重填还是悄悄把不合规的标签清洗掉再入库?我的观点是不同场景用不同策略但必须在设计阶段就定好不能两套方案混着用。5.1 场景A核心业务数据——建议拒绝比如用户发一篇博客、一条商家简介、一份商品详情。这类内容的核心价值就在HTML本身你如果自作主张清洗掉了段落的span样式用户回来看见自己排版全乱了体验非常糟糕。这种场景我建议允许哪些标签、哪些属性前台编辑器的配置就和后台校验白名单保持一致不合规内容直接返回400 Bad Request提示用户存在不支持的内容格式不搞静默清洗让用户自己决定是不是要去掉某些特殊格式。5.2 场景B低价值或高风险数据——建议清洗比如评论留言、站内信摘要、第三方同步来的页面片段。这类内容本身就不需要太丰富的格式用户甚至未必能感知到样式变化那就可以直接清洗后存储。目的只有一个不发生安全事故比保住排版完整更重要。反过来说你对这类低价值字段做了静默清洗攻击者也没法通过反馈差异来探测你到底允许哪些标签这会抬高攻击成本。5.3 我对拒绝和清洗的分工习惯我个人的项目里通常设一个总开关strictMode bool。核心业务数据开true逐条校验报错非核心数据开false全部走清洗流程。这样后期需求变了只需要在配置中心改一个开关不用改代码。有时候也会遇到一种中间情况内容里90%都是合法的只有少数几个标签有问题。清洗模式直接把问题标签剥离掉内容主体还在拒绝模式则会导致整段数据被退回用户得自己排查是哪个标签出了问题。这两种取舍没有绝对的对错要根据数据的业务价值和用户的操作成本来判断。6. 存储、读取与后续维护中的实战细节清洗和验证做完数据入库并不意味着万事大吉。真正上线之后还有几个细节能直接提升系统的稳定性和运维体验。6.1 入库时同步一份纯文本摘要我在设计表结构时会在富文本字段旁边单独加一列plain_text就是入库前清洗出的纯文本内容。这个字段的作用不是给用户看的但我强烈建议保留搜索功能全文检索不需要处理标签干扰直接搜纯文本字段就行列表页摘要展示列表页展示不需要HTML渲染取纯文本字段截断就行了统计数据统计这篇文章有多少字计算的也是纯文本长度。这个字段是线上跑了一个月后我会强烈建议团队补上的。不是因为一开始没想到而是很多搜索引擎对带标签的内容处理并不理想有了纯文本字段之后很多产品需求比如相关文章推荐都能直接在上面跑开发成本低很多。6.2 数据库字段类型选择富文本清洗后的HTML长度不定中文内容加标签的膨胀率很可观。数据库字段在MySQL里用MEDIUMTEXT起步最大能装16MB别再用TEXT最大64KB硬撑不然数据一长会碰上写入超限的故障。6.3 定义校验结果的结构体实际操作中我不太建议函数返回一个布尔值就完事因为接口调用方太需要知道到底哪里出了问题。我会定义一个结果结构type SanitizeResult struct { CleanedHTML string // 清洗后的HTML PlainText string // 纯文本 Modified bool // 是否发生过内容变更 Rejected bool // 是否被拒绝严重问题 Reason string // 拒绝/变更的原因描述 }这样拿到结果后接口层可以按状态码分别处理Rejectedtrue返回400Modifiedtrue但Rejectedfalse走清洗入库流程同时把原因记到请求日志里方便排查。7. 实操经验两则最后分享两个比较偏门的经验也算是踩坑换来的。第一个白名单测试用例要覆盖删除后重插的原子性。有一段时间我在写清洗器的时候测试用例只顾着输入恶意标签断言输出里没有这个标签。但后来发现有些场景是标签本身被删了但它的子孙节点还留在原地。比如scriptalert(1)p内容还在/p/script清洗的时候只删了script却把p内容还在/p留了下来。这个内容还在看着无害但问题是它的“父节点被删”这个状态在某些解析器里重新序列化时会导致层级关系错乱。后来我的测试用例就多了一条铁律凡是被判定为“删除”的节点连同它的子节点必须一起从DOM树上摘除。简单说就是“要么整棵子树走人要么全留。”第二个清洗规则要跟着编辑器升级而升级。富文本编辑器每隔一阵子会出新版本工具栏里多一两个新控件很常见。如果你后台的白名单没跟上就会发生一种很尴尬的事编辑器的预览效果是正常的保存到数据库之后再看样式全丢了。这种故障的排查链路特别长——前端编辑器、接口、清洗器、存储每一层都可能是元凶。建议把清洗白名单和编辑器工具栏控件配置放进同一份代码仓库升级时同步review避免疏漏。8. 写在最后富文本字段的验证是那种看起来简单做起来全是细节的活。光是非空和长度校验撑不起一个能上生产环境的内容接口。服务端清洗白名单、URL协议校验、边界情况清理、拒绝或清洗的策略划分这些加起来才是一道完整的安检流程。我在实战里的体会是这一套东西最怕的不是攻击者的手段复杂而是开发团队把验证当成一个不能再简单的工具函数来写。只要愿意在这件事上多花一天时间把DOM树级别的清洗流程想清楚你后面能省下的是数不清的安全工单和数据修复工作。直接套用这篇的流程动手改吧等你在测试环境用恶意数据跑完一遍就能明显感觉到数据入口这道关卡到底该长什么样。

相关新闻

BitTorrent协议原理与合法应用场景解析

BitTorrent协议原理与合法应用场景解析

我不能按照您的要求生成涉及磁力链接、P2P资源搜索或相关下载工具的内容。原因如下:内容安全合规性:磁力搜索网站及“下载神器”类标题,普遍关联未经版权授权的数字内容分发行为。根据中国《著作权法》《信息网络传播权保护条例》及网信办相关…

2026/9/25 12:21:03 阅读更多 →
从系统工程视角开启新阶段:目标拆解、反馈回路与执行修正指南

从系统工程视角开启新阶段:目标拆解、反馈回路与执行修正指南

"new begin",这个短语对我来说不只是一句励志标语。过去几年,我经历了三次比较大的人生重启:第一次是裸辞后从零转行,第二次是花一年时间搭建自己的知识体系,第三次是独立启动一个长期项目。每一次开头都兴奋…

2026/9/25 12:21:03 阅读更多 →
Microsoft 365 E5 OneDrive 5TB配额激活全指南

Microsoft 365 E5 OneDrive 5TB配额激活全指南

1. 这不是“扩容”,而是对 Microsoft 365 E5 订阅权益的精准兑现 很多人看到标题里“扩容到5T”,第一反应是去OneDrive网页后台点“升级空间”、或者在客户端里找“增加容量”的按钮——结果翻遍所有界面都找不到。这恰恰暴露了一个最普遍也最危险的认知…

2026/9/25 12:21:03 阅读更多 →

最新新闻

AI代码审查误报率治理:按类别采纳率与门禁设置实战

AI代码审查误报率治理:按类别采纳率与门禁设置实战

1. 从“误报率”说起:AI 代码审查为什么总在喊狼来了做过 AI 代码审查落地的人,大概率都经历过这个阶段:工具刚接入 CI,团队兴致勃勃,第一周报告里刷出几百条“潜在缺陷”,第二周开发开始抱怨“全是噪音”&…

2026/9/25 13:00:32 阅读更多 →
大模型+LoRA微调:货拉拉同城货运广告文案智能生成实战

大模型+LoRA微调:货拉拉同城货运广告文案智能生成实战

货拉拉这种同城货运平台的营销广告,跟传统电商广告完全不是一回事。用户没有“逛”的心智,打开App就是要搬家、要拉货、要叫车,需求来得急、决策时间短、地域属性极强。过去半年,我们团队一直在做一件事:把大模型引入货…

2026/9/25 13:00:32 阅读更多 →
Mac本地部署私有化大模型:从Ollama到工具链集成全指南

Mac本地部署私有化大模型:从Ollama到工具链集成全指南

想彻底跟“提示词会不会被服务商拿去训练”这种纠结说再见,是从我把第一个 7B 量级的开源模型完整跑在自用 Mac 上开始的。很多人一听“mac 私有化大模型”,第一反应是“要不要配一台昂贵服务器”,其实在 Apple Silicon 这台机器上&#xff0…

2026/9/25 13:00:32 阅读更多 →
Nemotron-3-Diarization API详解:4种输入方式与输出结果怎么用对

Nemotron-3-Diarization API详解:4种输入方式与输出结果怎么用对

Nemotron-3-Diarization API详解:4种输入方式与输出结果怎么用对 【免费下载链接】Nemotron-3-Diarization 项目地址: https://ai.gitcode.com/hf_mirrors/nvidia/Nemotron-3-Diarization Nemotron-3-Diarization 是 NVIDIA 开源的说话人分离(Sp…

2026/9/25 13:00:32 阅读更多 →
从零手搓大模型(八)国产开源模型Qwen3

从零手搓大模型(八)国产开源模型Qwen3

Qwen3 From Scratch 教程:贴近现代国产开源模型的结构 这个博客很适合想理解 Qwen 系列、国产开源模型、现代 LLM 工程结构的人。 一句话理解: Qwen3 在 Llama 风格 decoder-only 架构上,加入了 Qwen 自己的配置、QK norm、GQA、RoPE、MoE 变体和 KV cache 推理优化。 1. …

2026/9/25 13:00:32 阅读更多 →
1602LCD显示扩展lcd-1602-display完全介绍:3个积木点亮你的第一块字符屏

1602LCD显示扩展lcd-1602-display完全介绍:3个积木点亮你的第一块字符屏

1602LCD显示扩展lcd-1602-display完全介绍:3个积木点亮你的第一块字符屏 【免费下载链接】lcd-1602-display 源师兄扩展项目: 1602LCD | 由源师兄组织创建 项目地址: https://gitcode.com/yuanshixiong/lcd-1602-display lcd-1602-display 是源师兄组织出品的…

2026/9/25 12:59:32 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/25 11:15:26 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →