SQL注入(SQL injection)是一种注入式攻击(injection attack)。当攻击者提交恶意构造的输入信息,导致应用程序执行非预期的操作时,即发生了注入攻击。由于SQL数据库的广泛使用,SQL注入是互联网上最常见的攻击类型之一。
如果您只能防范一种漏洞,您最应该检查的就是代码库中是否存在SQL注入漏洞!
SQL注入攻击的致命后果
当遭受SQL注入攻击时,最严重的后果可能是什么?
我们的示例攻击向您展示了如何绕过登录页面:这对于一个银行网站来说是一个巨大的安全隐患。更复杂的攻击甚至能让攻击者在数据库上执行任意(恶意的)SQL语句。在过去,黑客曾通过注入攻击进行以下操作:
- 提取敏感信息,如身份证号或信用卡信息。
- 列举网站注册用户的认证凭证,以便利用这些登录信息攻击其他网站。
- 删除数据或删除(DROP)数据库表,破坏数据库,导致网站无法使用。
- 注入进一步的恶意代码,以便在用户访问网站时执行。
SQL注入攻击的发生频率之高令人咋舌。像雅虎(Yahoo)和索尼(Sony)这样的大公司都曾遭遇其应用程序被入侵的案例。在其他案例中,黑客组织会针对特定的应用系统进行攻击,或编写脚本试图窃取认证凭证。甚至连安全公司也未能幸免!
参数化语句 (Parameterized Statements)
编程语言通过数据库驱动程序(database drivers)与 SQL 数据库进行通信。驱动程序允许应用程序对数据库构造并执行 SQL 语句,按需提取和操作数据。参数化语句 (Parameterized statements) 能确保传入 SQL 语句的参数(即输入值)得到安全处理。
例如,在 JDBC 中使用参数化语句安全执行 SQL 查询的方法如下:
// 连接到数据库。
Connection conn = DriverManager.getConnection(URL, USER, PASS);
// 构造我们要运行的 SQL 语句,指定参数占位符 ?。
String sql = "SELECT * FROM users WHERE email = ?";
// 用占位符参数生成一个预处理语句 (PreparedStatement)。
PreparedStatement stmt = conn.prepareStatement(sql);
// 将 email 值绑定到语句的第 1 个参数索引上。
stmt.setString(1, email);
// 运行查询...
ResultSet results = stmt.executeQuery(); // 注意:这里应该是 stmt.executeQuery(),不带 sql 字符串参数
while (results.next())
{
// ...对返回的数据进行处理。
}
将此与显式拼接 SQL 字符串的做法对比,后者极其极其危险:
// 我们想要查找的用户。
String email = "user@email.com";
// 连接到数据库。
Connection conn = DriverManager.getConnection(URL, USER, PASS);
Statement stmt = conn.createStatement();
// 糟糕,非常糟糕!不要用字符串拼接构建查询!
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
// 我有种不祥的预感...
ResultSet results = stmt.executeQuery(sql);
while (results.next()) {
// ...哦,我们被入侵了。
}
关键区别在于传递给 executeQuery(...) 方法的数据。在第一种(参数化)情况下,包含占位符的 SQL 字符串和实际的参数值是分别传递给数据库驱动程序的,这使驱动程序能够正确地解释它们。在第二种(拼接字符串)情况下,完整的 SQL 语句在调用驱动程序之前就已经构造好了,这意味着我们很容易受到恶意构造的参数攻击。
您应当始终优先使用参数化语句,它们是您防范 SQL 注入的头号防护手段。
您可以在下面的代码示例中查看各种语言中参数化语句的更多例子。
对象关系映射 (Object Relational Mapping - ORM)
许多开发团队倾向于使用对象关系映射 (ORM) 框架,以使 SQL 结果集到代码对象的转换更加无缝。ORM 工具通常意味着开发人员几乎不需要在代码中手写 SQL 语句——值得庆幸的是,这些工具在底层也使用了参数化语句。
最著名的 ORM 可能是 Ruby on Rails 的 Active Record 框架。使用 Active Record 从数据库获取数据如下所示:
def current_user(email)
# 'User' 对象是一个 Active Record 对象,Rails 为其魔法般自动生成了查找方法。
User.find_by_email(email)
end
类似这样的代码可以免受 SQL 注入攻击。
然而,使用 ORM 并不能自动让您完全免疫 SQL 注入。 许多 ORM 框架允许您在需要对数据库执行更复杂操作时构造 SQL 语句或 SQL 语句片段。例如,以下 Ruby 代码就容易受到注入攻击:
def current_user(email)
# 这段代码在面对恶意构造的 email 参数时就存在漏洞。
User.where("email = '" + email + "'") # 字符串拼接,危险!
end
作为一个经验法则:当你发现自己正在通过拼接字符串来编写 SQL 语句时,请仔细思考你正在做的事情!
转义输入 (Escaping Inputs)
如果您无法使用参数化语句或能够为您生成 SQL 的库(如 ORM),次优的方法是确保对输入参数中的特殊字符串字符进行正确的转义 (escaping)。
注入攻击常常依赖于攻击者能够精心构造一个输入,该输入会提前关闭其在 SQL 语句参数中出现的那个字符串。(这就是为什么在尝试进行 SQL 注入攻击时您经常会看到 ' 或 " 等字符。)
编程语言描述字符串内包含引号的标准方式——SQL 在这方面也不例外。通常,将引号字符加倍(例如,将 ' 替换为 '')意味着“将此引号视为字符串的一部分,而不是字符串的结束符”。
转义特殊字符是防范大多数 SQL 注入攻击的一种简单方法,许多语言都提供标准函数来实现此目的。但是,这种方法有几个缺点:
- 需要非常小心:您必须确保在代码库中所有构造 SQL 语句的地方都进行了转义。
- 并非所有攻击都依赖引号:并非所有的注入攻击都依赖于滥用引号字符。例如,当 SQL 语句中预期是数字 ID 时,就不需要引号字符。无论您如何巧妙地处理引号字符,以下代码仍然存在注入漏洞:
def current_user(id)
User.where("id = " + id) # id 参数未被转义且直接拼接,危险!
end
清理输入 (Sanitizing Inputs)
对所有应用程序来说,清理输入 (sanitizing inputs) 都是一个好的实践。在我们之前演示的黑客攻击案例中,用户输入的密码是 ' or 1=1--,这看起来是一个非常可疑的密码选择。
开发人员应该始终努力拒绝那些看起来可疑的输入,同时注意不要意外地惩罚合法用户。例如,您的应用程序可以通过以下方式清理 GET 和 POST 请求中提供的参数:
- 检查提供的字段(如电子邮件地址)是否符合正则表达式。
- 确保数字或字母数字字段不包含符号字符。
- 在不合适的地方拒绝(或去除)空格和换行符。
- 客户端验证(如使用 JavaScript)对于在用户填写表单时提供即时反馈很有用,但它并不能防御真正的黑客。 因为大多数黑客攻击尝试都是使用脚本(而非浏览器本身)来进行的。
以下代码示例展示了防范 SQL 注入时应遵循的良好实践与应避免的危险做法:
Node.js 示例
node-sql 库 (安全 - 默认参数化)
var sql = require('sql');
var user = sql.define({...}); // 假设已定义用户表结构
// 默认以参数化方式构建查询
var query = user.select(user.star())
.from(user)
.where(user.email.equals(email)) // ✅ 安全
.toQuery(); // 将生成安全的参数化查询
mysql 库 (安全 - 显式参数化)
var mysql = require('mysql');
var connection = mysql.createConnection({...});
connection.connect();
// 查询语句与参数分开传递 ✅ 安全
connection.query(
'SELECT * FROM users WHERE email = ?', // 使用 ? 占位符
[email], // 参数值单独传入数组
function(err, rows) {...}
);
connection.end();
pg (PostgreSQL) 库 (安全 - 显式参数化)
var pg = require('pg');
var client = new pg.Client("postgres://...");
client.connect(function(err) {
// 查询语句与参数分开传递 ✅ 安全
client.query(
'SELECT * FROM users WHERE email = $1', // PostgreSQL 使用 $1, $2... 占位符
[email],
function(err, result) {...}
);
});
client.end();
Python 示例
DB API 2.0 (安全 - 参数化)
# SQL 与参数分开传递给驱动 ✅ 安全
cursor.execute("SELECT user_id, user_name FROM users WHERE email = ?", (email,))
# 字符串拼接方式危险 ❌
cursor.execute("SELECT user_id, user_name FROM users WHERE email = '%s'" % email)
# 直接拼接,易受注入攻击
Django ORM (安全 - ORM/参数化)
# 使用原生 ORM 语法 ✅ 安全
Users.objects.filter(email=email)
# 使用 Raw SQL 但参数化 ✅ 安全
Users.objects.raw("SELECT * FROM users WHERE email = %s", [email])
# 字符串拼接危险 ❌
Users.objects.raw("SELECT * FROM users WHERE email = '%s'" % email) # 易受攻击
Ruby 示例
Active Record (安全 - ORM 方法)
# 使用内置查找方法 ✅ 安全
def current_user(email)
User.find_by_email(email)
end
# 使用字符串拼接条件 ❌
def current_user(email)
User.where("email = '" + email + "'") # 高危!拼接导致注入漏洞
end
Sequel ORM (安全与危险对比)
# 安全:使用哈希条件 ✅
def current_user(email)
User.where(email: email)
end
# 危险:字符串插值 ❌
def current_user(email)
User.where("email = #{params[:email]}") # 插值等于拼接,不安全
end
Java 示例
JDBC (安全 - PreparedStatement)
// 使用 PreparedStatement 与 ? 占位符 ✅ 安全
String sql = "SELECT * FROM users WHERE email = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, email); // 安全绑定参数
ResultSet results = stmt.executeQuery(); // 注意:此处应无参数!修复原代码
JDBC (危险 - 字符串拼接)
// 字符串拼接构建 SQL ❌ 高危!
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
Statement stmt = conn.createStatement();
ResultSet results = stmt.executeQuery(sql); // 等待被攻击...
Hibernate ORM (安全 - 自然键查询)
@Entity
public class User {
@NaturalId String email;
}
// 使用 ORM 安全加载 ✅
return session.bySimpleNaturalId(User.class).load(email);
Spring JDBC Template (安全 - 参数化)
String sql = "SELECT * FROM users WHERE email = ?";
// 查询语句与参数分开传递 ✅
User user = getJdbcTemplate().queryForObject(sql, new Object[]{email}, new UserRowMapper());
C# (.NET) 示例
SqlClient (安全 - 参数化)
SqlCommand command = new SqlCommand("SELECT * FROM Users WHERE email = @email", conn);
command.Parameters.Add(new SqlParameter("email", email)); // ✅ 使用命名参数
using (SqlDataReader reader = command.ExecuteReader()) {...}
LINQ to SQL (安全 - ORM)
// LINQ 自动参数化 ✅
var users = from user in ctx.Users
where user.Email == email // 编译器生成安全查询
select user;
PHP 示例
PDO (安全 - 参数化)
$statement = $dbh->prepare("SELECT * FROM users WHERE email = ?");
$statement->execute([$email]); // 参数数组绑定 ✅ 安全
其他关键安全实践
最小权限原则 (Principle of Least Privilege)
- 应用程序应确保每个进程/组件仅能访问其必需资源(如同银行金库仅限授权员工进入)。
- 运行时降权:生产环境数据库账号禁用 DDL 语句(如
CREATE,ALTER,DROP),仅允许执行 DML(SELECT,INSERT,UPDATE,DELETE)。数据表结构变更应在发布窗口使用临时高权限账号执行。 - 精细化权限:对于复杂系统:
- 限制仅能通过存储过程修改数据。
- 对查询服务使用只读账号。
- 纵深防御效果:即便攻击者入侵系统,也能最大限度限制其破坏范围。
密码哈希存储 (Password Hashing)
- 绝对避免明文存储密码,此为重大安全隐患。
- 必须使用强单向哈希算法(如 Argon2, bcrypt, scrypt)处理密码。
- 务必加盐 (Salted) 以防范彩虹表攻击。
- 安全价值:即使凭据泄露,攻击者也难以还原原始密码或冒用用户身份。
第三方认证集成 (Third-Party Authentication)
- 考虑外包认证流程:利用Google等提供的 OAuth API。
- 核心优势:
- 开发便捷:避免自行实现认证系统。
- 用户信任:用户使用现有平台账号登录,密码仅存储于该平台(单一保管点)。
- 安全提升:依托大厂成熟、持续维护的安全基础设施。
总结要点:
- 参数化语句(Prepared Statements)是黄金准则,在所有语言/库中优先使用。
- ORM 框架能自动防御注入,但需避免在其内部手动拼接 SQL。
- 最小权限原则为第二道防线,显著降低入侵后的破坏力。
- 密码必须加盐哈希存储,彻底杜绝明文密码风险。
- 第三方认证是兼顾安全与用户体验的优选方案。
2241

被折叠的 条评论
为什么被折叠?



