SQL注入:最危险的Web漏洞之一

SQL注入(SQLi)自OWASP Top 10诞生以来一直名列前茅。尽管防御方法已经非常成熟,但它仍然是最常被利用的Web安全漏洞之一。HackerOne的漏洞赏金报告显示,SQL注入漏洞的平均赏金高达3,000美元,严重的案例可达数万美元。

2024年,MOVEit Transfer的SQL注入漏洞(CVE-2023-34362)导致全球数千家组织的数据泄露。这个案例再次证明,SQL注入在今天仍然是一个严重的安全威胁。

SQL注入的原理

SQL注入发生在应用程序将用户输入直接拼接到SQL查询中,使攻击者能够操纵数据库查询的逻辑。

一个经典的示例

# 危险的代码 - SQL注入漏洞
username = request.GET['username']
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)

当攻击者输入 ' OR '1'='1 时,查询变成:

SELECT * FROM users WHERE username = '' OR '1'='1'

这将返回所有用户记录,因为 '1'='1' 永远为真。

SQL注入类型

类型英文名特征危害检测难度
经典注入In-band SQLi直接回显结果
联合查询Union-based使用UNION拼接数据
报错注入Error-based利用错误信息
盲注Blind SQLi基于布尔/时间判断
带外注入Out-of-band通过其他渠道获取数据极高
二次注入Second-order存储后在其他查询中触发极高

防御方案

方案一:参数化查询(最推荐)

参数化查询(也叫预处理语句)是防御SQL注入最有效的方法。它将SQL结构和数据严格分离。

Python (SQLAlchemy)

from sqlalchemy import text

# 正确 - 参数化查询
stmt = text("SELECT * FROM users WHERE username = :username AND status = :status")
result = db.execute(stmt, {"username": username, "status": "active"})

# 使用ORM(推荐)
user = session.query(User).filter(User.username == username).first()

PHP (PDO)

// 正确 - PDO预处理语句
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();

// 确保PDO的仿真预处理关闭
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Java (PreparedStatement)

// 正确 - PreparedStatement
String sql = "SELECT * FROM users WHERE username = ? AND role = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, role);
ResultSet rs = stmt.executeQuery();

Go

// 正确 - 参数化查询
row := db.QueryRow("SELECT id, name FROM users WHERE username = $1", username)
err := row.Scan(&id, &name)

方案二:输入验证

import re

def validate_username(username: str) -> bool:
    """验证用户名只包含允许的字符"""
    # 白名单验证:只允许字母、数字和下划线
    pattern = r'^[a-zA-Z0-9_]{3,30}$'
    return bool(re.match(pattern, username))

def validate_integer(value: str) -> int:
    """确保输入是有效的整数"""
    try:
        return int(value)
    except (ValueError, TypeError):
        raise ValueError("Invalid integer input")

def validate_order_column(column: str) -> str:
    """白名单验证排序列名"""
    allowed = {'id', 'username', 'created_at', 'email'}
    if column not in allowed:
        raise ValueError(f"Invalid column: {column}")
    return column

方案三:ORM框架

使用ORM(对象关系映射)框架可以避免直接编写SQL:

# Django ORM
# 安全 - ORM自动处理参数化
users = User.objects.filter(
    username=request.GET['username'],
    is_active=True
).select_related('profile')

# 注意:raw()和extra()仍可能引入注入
# 避免使用或确保参数化
User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id])

方案四:Web应用防火墙(WAF)

WAF作为额外防线,可以检测和阻止SQL注入尝试:

# ModSecurity(Nginx WAF)基本SQL注入防护规则
# /etc/modsecurity/rules/sql-injection.conf

SecRule ARGS|ARGS_NAMES|REQUEST_COOKIES "@detectSQLi" \
    "id:942100,\
    phase:2,\
    block,\
    msg:'SQL Injection Attack Detected',\
    logdata:'Matched Data: %{TX.0}',\
    severity:'CRITICAL',\
    tag:'OWASP_CRS'"

安全编码检查清单

sql_injection_prevention_checklist:
  必须做:
    - 所有数据库查询使用参数化语句
    - 对用户输入进行白名单验证
    - 使用ORM框架而非原始SQL
    - 数据库账号使用最小权限原则
    - 错误信息不泄露数据库结构

  应该做:
    - 部署WAF作为额外防线
    - 使用自动化工具定期扫描SQL注入漏洞
    - 对开发人员进行安全编码培训
    - 代码审查中关注数据库查询

  不要做:
    - 拼接用户输入到SQL字符串中
    - 依赖黑名单过滤(如过滤单引号)
    - 使用数据库管理员账号运行应用
    - 在生产环境显示详细错误信息

测试与检测

使用漏洞扫描工具定期检测SQL注入漏洞:

# 使用sqlmap测试SQL注入(仅限授权测试)
sqlmap -u "http://target.com/page?id=1" --batch --level=3 --risk=2

# 使用OWASP ZAP进行自动化扫描
# zap-cli quick-scan -s all http://target.com

关于更多Web安全的防护措施,可以参考网络安全入门指南中的Web安全部分。

总结

SQL注入防御的核心就是一个原则:永远不要将用户输入直接拼接到SQL查询中。使用参数化查询,配合输入验证和最小权限原则,就能有效防御绝大多数SQL注入攻击。安全编码不是额外的工作,而是专业开发的基本素养。