正在加载,请稍候…

SQL注入:工作原理与防御方法

通过真实示例了解SQL注入攻击的工作原理,以及参数化查询、ORM和输入验证等精确防御技术

SQL注入:工作原理与防御方法

什么是SQL注入?

SQL注入是一种代码注入攻击,攻击者将SQL命令插入输入字段,然后由数据库执行。二十多年来,它一直是排名第一的Web应用程序漏洞,并且仍然频繁出现在安全事件报告中。

核心问题:通过拼接用户输入来构建SQL查询的应用程序,允许攻击者修改查询的逻辑。

SQL注入:工作原理与防御方法 插图

经典示例

考虑一个登录表单,其后端查询如下:

// ❌ 存在漏洞——永远不要这样做
const query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

正常输入 username = "alice" 时,查询变为:

SELECT * FROM users WHERE username = 'alice' AND password = 'secret'

现在攻击者输入 username = "' OR '1'='1' --"

SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = '...'

-- 注释掉了查询的其余部分。'1'='1' 始终为真。攻击者无需知道任何密码即可登录为数据库中的第一个用户——通常是管理员。

更危险的注入模式

数据提取(UNION攻击)

' UNION SELECT username, password, null FROM users --

如果应用程序显示查询结果,攻击者可以从任何表中提取任何数据。

盲SQL注入

当应用程序不显示查询结果时,攻击者使用时间或布尔响应逐位提取数据:

' AND SLEEP(5) --          -- MySQL:如果存在漏洞则延迟5秒
' AND 1=(SELECT 1 FROM users WHERE username='admin' AND SUBSTRING(password,1,1)='a') --

SQL注入:工作原理与防御方法 插图

数据库破坏

'; DROP TABLE users; --
'; DELETE FROM orders WHERE 1=1; --

防御:参数化查询(预编译语句)

根本修复:永远不要将用户输入拼接到SQL中。使用参数化查询,其中SQL结构固定,数据单独传递。

// ✅ Node.js with pg (PostgreSQL)
const { rows } = await pool.query(
  'SELECT * FROM users WHERE username = $1 AND password_hash = $2',
  [username, passwordHash]
);

// ✅ Node.js with mysql2
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE username = ? AND password_hash = ?',
  [username, passwordHash]
);

// ✅ Node.js with better-sqlite3
const stmt = db.prepare('SELECT * FROM users WHERE username = ? AND password_hash = ?');
const user = stmt.get(username, passwordHash);

使用参数化查询时,数据库分别接收SQL结构和参数。即使参数包含SQL语法,它也被视为字面字符串——永远不会作为SQL命令执行。

# ✅ Python with psycopg2 (PostgreSQL)
cur.execute(
    "SELECT * FROM users WHERE username = %s AND password_hash = %s",
    (username, password_hash)
)

# ✅ Python with SQLite
cur.execute(
    "SELECT * FROM users WHERE username = ? AND password_hash = ?",
    (username, password_hash)
)

# ✅ Python with SQLAlchemy (ORM)
from sqlalchemy import text
result = db.execute(
    text("SELECT * FROM users WHERE username = :username"),
    {"username": username}
)
// ✅ Go with database/sql
row := db.QueryRow(
    "SELECT id, username FROM users WHERE username = $1",
    username,
)

ORM:默认参数化

对象关系映射器自动生成参数化查询,使安全路径成为简单路径:

// ✅ Prisma — 默认安全
const user = await prisma.user.findFirst({
  where: {
    username: username,  // 自动参数化
    passwordHash: passwordHash,
  },
});

// ✅ Sequelize
const user = await User.findOne({
  where: { username, passwordHash },
});

// ✅ TypeORM
const user = await userRepo.findOne({
  where: { username, passwordHash },
});

警告: 如果使用带字符串插值的原始查询方法,ORM仍然可能存在漏洞:

// ❌ 仍然存在漏洞——在ORM中使用原始SQL
const user = await prisma.$queryRaw`SELECT * FROM users WHERE username = ${username}`;
// 等等——这实际上是安全的!Prisma使用带自动参数化的标签模板字面量

// ❌ 这确实存在漏洞:
const user = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE username = '${username}'`  // 永远不要这样做
);

输入验证:纵深防御

参数化查询是主要防御手段。输入验证增加了第二层:

// 白名单验证——只接受预期字符
function validateUsername(username) {
  if (!/^[a-zA-Z0-9_]{3,50}$/.test(username)) {
    throw new Error('用户名格式无效');
  }
  return username;
}

// 类型检查
function validateUserId(id) {
  const parsed = parseInt(id, 10);
  if (isNaN(parsed) || parsed <= 0) {
    throw new Error('用户ID无效');
  }
  return parsed;
}

验证减少了攻击面,但永远不能替代参数化查询。攻击者会找到绕过验证的创造性方法;参数化查询在结构上是安全的。

SQL注入:工作原理与防御方法 插图

动态查询:处理列名和表名

参数适用于值,但不适用于列名、表名或SQL关键字。如果必须动态构建这些,请使用白名单

// ✅ 安全:动态列名的白名单方法
const ALLOWED_SORT_COLUMNS = new Set(['name', 'email', 'created_at', 'updated_at']);

function buildSortQuery(column, direction) {
  if (!ALLOWED_SORT_COLUMNS.has(column)) {
    throw new Error(`无效的排序列:${column}`);
  }
  const dir = direction === 'DESC' ? 'DESC' : 'ASC';  // 强制有效值
  return `ORDER BY ${column} ${dir}`;  // 安全:两个值都来自白名单
}

const sql = `SELECT * FROM users ${buildSortQuery(req.query.sort, req.query.dir)}`;

永远不要使用黑名单(试图过滤掉坏字符)。攻击者知道数百种编码技术可以绕过它们。

最小权限:限制注入发生时的损害

即使使用参数化查询,纵深防御也很有价值:

-- 为应用程序创建一个受限数据库用户
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';

-- 只授予应用程序所需权限
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app_user'@'localhost';
GRANT SELECT, INSERT ON myapp.orders TO 'app_user'@'localhost';

-- 不要授予DELETE、DROP或系统表访问权限
REVOKE ALL ON *.* FROM 'app_user'@'localhost';

如果注入攻击以某种方式成功,受限数据库用户会限制攻击者能做什么。

检测SQL注入漏洞

手动测试: 在输入字段中尝试以下内容:

'
''
'--
' OR '1'='1
' OR 1=1 --

如果应用程序抛出数据库错误或行为不同,则可能存在漏洞。

自动扫描: OWASP ZAP、sqlmap(仅用于授权测试)、Burp Suite Pro。

代码审查: 搜索带有SQL的字符串拼接:

grep -r "SELECT.*+.*req." src/
grep -r "WHERE.*`${" src/

总结

方法 有效性 使用时机
参数化查询 ✅ 主要防御 始终——用于所有用户输入值
ORM ✅ 默认安全 用于标准CRUD操作
输入验证 ✅ 纵深防御 在参数化之外额外使用
动态SQL白名单 ✅ 用于标识符 列名、表名、ORDER BY
转义/编码 ⚠️ 仅作为最后手段 仅在无法参数化时
黑名单过滤 ❌ 不足 永远不要单独依赖

→ 在开发过程中使用 SQL格式化工具 检查和格式化SQL查询。