在Web开发中,很多场景需要把用户提交并存储到数据库的富文本内容展示在前端页面,比如博客文章、评论内容、产品介绍等。富文本通常包含自定义的HTML标签和样式,直接输出到页面会带来严重的安全隐患,最典型的就是跨站脚本攻击(XSS)。攻击者可以在富文本中插入恶意脚本,当其他用户访问包含该内容的页面时,脚本就会被执行,窃取用户的登录凭证、篡改页面内容甚至发起进一步的攻击。

富文本渲染的核心安全风险
数据库存储的富文本内容未经过安全处理就直接渲染,主要会带来以下几类风险:
- XSS攻击:攻击者在富文本中插入
<script>标签或者事件属性(如onclick、onerror),执行恶意JavaScript代码。 - 样式篡改:通过插入自定义CSS样式,改变页面布局,甚至引导用户点击恶意内容。
- 资源滥用:插入大量图片、视频等外部资源,消耗用户带宽,或者加载恶意外部资源。
安全渲染的核心方案
1. HTML转义(适用于纯文本展示场景)
如果不需要保留富文本的格式,只需要展示文本内容,最简单的方式是对所有HTML特殊字符进行转义,把<、>、&等字符转换成对应的实体编码,浏览器就不会把它们解析为HTML标签。
以下是JavaScript实现的转义函数示例:
// HTML转义函数,将特殊字符转为实体编码
function escapeHtml(unsafeStr) {
if (typeof unsafeStr !== 'string') {
return unsafeStr;
}
return unsafeStr
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 从数据库获取的富文本内容
const richTextFromDb = '<script>alert("xss")</script><p>这是一段测试内容</p>';
// 转义后渲染
const safeText = escapeHtml(richTextFromDb);
// 输出到页面,此时script标签不会被解析
document.getElementById('content').innerHTML = safeText;
2. 白名单过滤(适用于需要保留部分富文本格式的场景)
如果需要保留合理的HTML标签(比如<p>、<img>、<strong>等),直接转义会丢失所有格式,这时需要使用白名单过滤的方式,只允许指定的标签和属性存在,移除所有不在白名单内的内容和属性。
以下是使用DOM API实现简单白名单过滤的示例:
// 定义允许的标签和对应的允许属性
const whiteList = {
p: [],
strong: [],
em: [],
img: ['src', 'alt', 'width', 'height'],
a: ['href', 'title']
};
// 白名单过滤函数
function filterRichText(htmlStr) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlStr;
// 递归处理所有节点
function walkNodes(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
// 如果标签不在白名单,用其文本内容替换该节点
if (!whiteList[tagName]) {
const textNode = document.createTextNode(node.textContent);
node.parentNode.replaceChild(textNode, node);
return;
}
// 移除不在允许列表中的属性
const allowedAttrs = whiteList[tagName];
const attrs = Array.from(node.attributes);
attrs.forEach(attr => {
if (!allowedAttrs.includes(attr.name)) {
node.removeAttribute(attr.name);
}
});
// 处理子节点
Array.from(node.childNodes).forEach(child => walkNodes(child));
}
}
walkNodes(tempDiv);
return tempDiv.innerHTML;
}
// 测试:包含恶意脚本和合法标签的内容
const testHtml = '<script>alert(1)</script><p>合法内容</p><img src="https://ipipp.com/test.jpg" onerror="alert(2)"><a href="javascript:alert(3)">恶意链接</a>';
const safeHtml = filterRichText(testHtml);
console.log(safeHtml);
// 输出:<p>合法内容</p><img src="https://ipipp.com/test.jpg"><a>恶意链接</a>
3. 内容安全策略(CSP)
除了对内容本身做处理,还可以通过设置内容安全策略(CSP)来进一步降低风险。CSP是浏览器的一个安全机制,通过HTTP响应头指定页面可以加载哪些资源、可以执行哪些脚本,即使页面中出现了恶意脚本,也会被CSP拦截。
常见的CSP配置示例:
# 禁止执行内联脚本,只允许加载同源的脚本,禁止加载外部恶意资源 Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://ipipp.com;
不同后端语言的处理示例
Java实现HTML转义
Java中可以使用Spring框架自带的HtmlUtils工具类,或者Apache Commons Text的StringEscapeUtils实现转义:
import org.springframework.web.util.HtmlUtils;
public class RichTextSafeRender {
public static void main(String[] args) {
String richTextFromDb = "<script>alert('xss')</script><p>测试内容</p>";
// 转义HTML特殊字符
String safeText = HtmlUtils.htmlEscape(richTextFromDb);
System.out.println(safeText);
// 输出:<script>alert('xss')</script><p>测试内容</p>
}
}
PHP实现白名单过滤
PHP可以使用内置的strip_tags函数配合允许标签列表,或者使用HTML Purifier第三方库做更完善的过滤:
// 简单过滤:只允许p、strong、em标签 $richTextFromDb = '<script>alert(1)</script><p>内容</p><strong>加粗</strong>'; $safeText = strip_tags($richTextFromDb, '<p><strong><em>'); echo $safeText; // 输出:<p>内容</p><strong>加粗</strong>
最佳实践总结
在实际开发中,建议根据场景组合使用多种方案:
- 如果不需要富文本格式,优先使用HTML转义,实现简单且安全。
- 如果需要保留富文本格式,必须使用成熟的白名单过滤库(比如前端用DOMPurify,后端对应语言的安全过滤库),不要自己写简单的过滤逻辑,很容易遗漏边界情况。
- 配合设置合理的CSP策略,作为第二层防护,即使过滤出现疏漏,也能降低攻击造成的影响。
- 对于用户上传的图片等资源,需要单独做校验,避免上传恶意文件或者包含恶意地址的资源。