404 Not Found

404 Not Found


nginx

SQL注入与安全

SQL注入与安全

🎯 生活类比

想象一个快递代收点:

📚 核心概念

1. SQL 注入原理

SQL 注入的本质是:用户输入被当作 SQL 代码执行

当程序用字符串拼接构造 SQL 时,攻击者可以插入恶意 SQL 片段。

SQL
-- 后端代码(伪代码)
sql = "SELECT * FROM users WHERE username = '" + input_user + "' AND password = '" + input_pass + "'"

-- 正常输入
-- input_user = "admin"
-- input_pass = "123456"
-- 生成的 SQL:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'

-- 恶意输入
-- input_user = "admin' --"
-- input_pass = "随便填"
-- 生成的 SQL:
SELECT * FROM users WHERE username = 'admin' --' AND password = '随便填'
-- '--' 是 SQL 注释,后面的条件被注释掉,绕过了密码验证!

2. 注入攻击常见手法

SQL
-- 1. 绕过登录验证
-- 输入: admin' OR '1'='1' --
SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = ''

-- 2. 联合查询窃取数据
-- 输入: ' UNION SELECT username, password FROM users --
SELECT * FROM products WHERE name = '' UNION SELECT username, password FROM users --'

-- 3. 堆叠注入执行多条语句
-- 输入: '; DROP TABLE users; --
SELECT * FROM products WHERE name = ''; DROP TABLE users; --'

-- 4. 布尔盲注(通过返回差异推断数据)
-- 输入: ' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --
SELECT * FROM products WHERE name = '' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --'

3. 参数化查询(Prepared Statement)

核心思想:SQL 结构和数据分离,数据库引擎不会把参数值当作 SQL 代码执行。

SQL
-- ❌ 字符串拼接(危险)
SET @username = "admin' --";
SET @sql = CONCAT('SELECT * FROM users WHERE username = ''', @username, '''');
PREPARE stmt FROM @sql;
EXECUTE stmt;

-- ✅ 参数化查询(安全)
SET @username = "admin' --";
PREPARE stmt FROM 'SELECT * FROM users WHERE username = ?';
EXECUTE stmt USING @username;
-- 数据库会把整个 @username 当作一个字符串值,不会执行其中的 SQL 片段

各语言的参数化查询:

PYTHON
# Python + MySQL Connector
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

# Python + SQLAlchemy ORM
user = session.query(User).filter(User.username == username, User.password == password).first()
JAVASCRIPT
// Node.js + mysql2
const [rows] = await pool.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);

// Node.js + Prisma ORM
const user = await prisma.user.findFirst({ where: { username, password } });
JAVA
// Java JDBC
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
PHP
// PHP PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute(['username' => $username, 'password' => $password]);

4. ORM 防注入

ORM(对象关系映射)框架通常自动使用参数化查询,但拼接原生 SQL 时仍需小心:

PYTHON
# ✅ ORM 自动参数化(安全)
User.objects.filter(username=username, password=password)

# ❌ ORM 中拼接原生 SQL(危险)
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")

# ✅ ORM 中使用原生 SQL 的安全写法
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
JAVASCRIPT
// ✅ Sequelize ORM 自动参数化
await User.findOne({ where: { username, password } });

// ❌ Sequelize 拼接查询(危险)
await sequelize.query(`SELECT * FROM users WHERE username = '${username}'`);

// ✅ Sequelize 安全写法
await sequelize.query("SELECT * FROM users WHERE username = ?", { replacements: [username] });

5. 最小权限原则

数据库用户只授予完成任务所需的最小权限:

SQL
-- 创建只读用户
CREATE USER 'app_readonly'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON mydb.* TO 'app_readonly'@'%';

-- 创建应用用户(只允许增删改查,不允许 DROP/ALTER)
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';

-- 禁止 GRANT 权限传递
REVOKE GRANT OPTION ON mydb.* FROM 'app_user'@'%';

-- 管理员用户(仅限内部管理)
CREATE USER 'db_admin'@'localhost' IDENTIFIED BY 'very_strong_password';
GRANT ALL PRIVILEGES ON mydb.* TO 'db_admin'@'localhost';

6. 防御清单

防御措施 说明
✅ 参数化查询 所有 SQL 查询使用参数占位符
✅ ORM 框架 优先使用 ORM,减少手写 SQL
✅ 输入验证 验证长度、类型、格式(白名单优于黑名单)
✅ 最小权限 数据库用户只授予必要权限
✅ 错误信息 不向用户暴露数据库错误详情
✅ WAF 防护 部署 Web 应用防火墙
✅ 定期审计 检查代码中的 SQL 拼接
❌ 字符串转义 不够可靠,不作为唯一防线

💡 基本语法

SQL
-- MySQL Prepared Statement
PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?';
SET @id = 1;
EXECUTE stmt USING @id;
DEALLOCATE PREPARE stmt;

-- MySQL LIKE 查询的安全写法
PREPARE stmt FROM 'SELECT * FROM users WHERE name LIKE ?';
SET @pattern = CONCAT('%', 'keyword', '%');
EXECUTE stmt USING @pattern;

-- IN 子句的安全写法(需要动态构造)
-- 方法:使用 FIND_IN_SET 或临时表
PREPARE stmt FROM 'SELECT * FROM users WHERE FIND_IN_SET(id, ?)';
SET @ids = '1,2,3';
EXECUTE stmt USING @ids;
💡 提示:永远不要信任用户输入。即使是下拉框、隐藏字段也可能被篡改,服务端必须对所有输入做校验和参数化处理。

示例:登录接口防注入改造(难度⭐)

改造前(危险):

PYTHON
# Python Flask - 危险写法
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    # ❌ 字符串拼接,存在 SQL 注入风险
    sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    user = db.execute(sql).fetchone()
    if user:
        return "登录成功"
    return "用户名或密码错误"
▶ 试一试

改造后(安全):

PYTHON
# Python Flask - 安全写法
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    # 输入验证
    if not username or len(username) > 50:
        return "用户名格式错误", 400
    if not password or len(password) > 100:
        return "密码格式错误", 400

    # ✅ 参数化查询
    user = db.execute(
        "SELECT * FROM users WHERE username = ? AND password = ?",
        (username, password)
    ).fetchone()

    if user:
        return "登录成功"
    return "用户名或密码错误"

示例:搜索功能防注入与性能优化(难度⭐⭐)

危险写法:

JAVASCRIPT
// Node.js Express - 危险写法
app.get('/search', (req, res) => {
    const keyword = req.query.q;
    // ❌ 拼接 SQL,存在注入风险
    const sql = `SELECT * FROM articles WHERE title LIKE '%${keyword}%' OR content LIKE '%${keyword}%'`;
    db.query(sql, (err, results) => {
        res.json(results);
    });
});
▶ 试一试

安全 + 优化写法:

JAVASCRIPT
// Node.js Express - 安全 + 优化写法
app.get('/search', async (req, res) => {
    try {
        const keyword = req.query.q?.trim();

        // 输入验证
        if (!keyword || keyword.length > 100) {
            return res.status(400).json({ error: '搜索关键词长度 1-100 个字符' });
        }

        // ✅ 参数化查询 + 全文索引优化
        const sql = `
            SELECT id, title, LEFT(content, 200) AS snippet, created_at
            FROM articles
            WHERE status = 1
              AND MATCH(title, content) AGAINST(? IN BOOLEAN MODE)
            ORDER BY MATCH(title, content) AGAINST(? IN BOOLEAN MODE) DESC
            LIMIT 20
        `;
        const [rows] = await pool.execute(sql, [keyword, keyword]);
        res.json(rows);
    } catch (err) {
        // 不暴露数据库错误详情
        console.error('Search error:', err);
        res.status(500).json({ error: '搜索服务暂时不可用' });
    }
});

🏢 场景一:用户注册接口安全防护

PYTHON
# Python - 安全的用户注册
import re
import hashlib

@app.route('/register', methods=['POST'])
def register():
    username = request.form.get('username', '').strip()
    email = request.form.get('email', '').strip()
    password = request.form.get('password', '')

    # 1. 输入验证(白名单)
    if not re.match(r'^[a-zA-Z0-9_]{3,50}$', username):
        return "用户名只能包含字母、数字和下划线,长度3-50", 400
    if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email) or len(email) > 100:
        return "邮箱格式不正确", 400
    if len(password) < 8 or len(password) > 100:
        return "密码长度8-100个字符", 400

    # 2. 参数化查询检查是否已存在
    existing = db.execute(
        "SELECT id FROM users WHERE username = ? OR email = ?",
        (username, email)
    ).fetchone()
    if existing:
        return "用户名或邮箱已被注册", 409

    # 3. 密码哈希存储
    password_hash = hashlib.sha256(password.encode()).hexdigest()

    # 4. 参数化插入
    db.execute(
        "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
        (username, email, password_hash)
    )
    db.commit()

    return "注册成功", 201

🏢 场景二:动态筛选条件的安全构建

JAVASCRIPT
// Node.js - 安全构建动态查询
function buildProductQuery(filters) {
    let sql = 'SELECT * FROM products WHERE 1=1';
    const params = [];

    if (filters.categoryId) {
        sql += ' AND category_id = ?';
        params.push(filters.categoryId);
    }

    if (filters.minPrice !== undefined) {
        sql += ' AND price >= ?';
        params.push(filters.minPrice);
    }

    if (filters.maxPrice !== undefined) {
        sql += ' AND price <= ?';
        params.push(filters.maxPrice);
    }

    if (filters.keyword) {
        sql += ' AND name LIKE ?';
        params.push(`%${filters.keyword}%`);
    }

    if (filters.status !== undefined) {
        sql += ' AND status = ?';
        params.push(filters.status);
    }

    // 排序字段白名单
    const allowedSort = ['price', 'created_at', 'sales_count'];
    const sortBy = allowedSort.includes(filters.sortBy) ? filters.sortBy : 'created_at';
    const sortOrder = filters.sortOrder === 'ASC' ? 'ASC' : 'DESC';
    sql += ` ORDER BY ${sortBy} ${sortOrder}`;

    // 分页参数验证
    const page = Math.max(1, parseInt(filters.page) || 1);
    const pageSize = Math.min(100, Math.max(1, parseInt(filters.pageSize) || 20));
    sql += ' LIMIT ? OFFSET ?';
    params.push(pageSize, (page - 1) * pageSize);

    return { sql, params };
}

// 使用
app.get('/products', async (req, res) => {
    const { sql, params } = buildProductQuery(req.query);
    const [rows] = await pool.execute(sql, params);
    res.json(rows);
});
💡 关键:排序字段不能直接用参数化占位符(因为是列名不是值),必须用白名单校验。

❓ 常见问题

Q:参数化查询会影响性能吗? A: 不会。参数化查询还能利用数据库的预编译缓存,多次执行同一模板的 SQL 时反而更快。

Q:ORM 就一定安全吗? A: 不一定。ORM 的自动参数化是安全的,但如果使用 raw() 或拼接字符串构造查询,仍然存在注入风险。框架只是工具,安全意识才是关键。

Q:存储过程能防注入吗? A: 存储过程本身不能防注入。如果存储过程内部使用了动态 SQL 拼接,同样存在注入风险。关键在于是否使用参数化。

Q:只用前端校验够吗? A: 绝对不够。前端校验可被绕过(直接调 API、修改请求)。所有安全校验必须在服务端执行。

📖 小节

本课深入讲解了 SQL 注入攻击与防御:

📝 作业

  1. 找出以下代码中的 SQL 注入风险并修复:
    SQL
    query = "SELECT * FROM products WHERE category = '" + category + "' ORDER BY " + sortField
    
  2. 为一个用户搜索功能编写安全的参数化查询代码(支持模糊搜索、分页、排序)。
  3. 为你的项目创建三个数据库用户:只读用户、应用用户、管理员用户,并分别授予合适的权限。

下一课→27-optimization.md

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏