web安全入门学习笔记11——SQL注入

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 注入攻击的一种简单方法,许多语言都提供标准函数来实现此目的。但是,这种方法有几个缺点:

  1. ​需要非常小心​​:您必须确保在代码库中所有构造 SQL 语句的地方都进行了转义。
  2. ​并非所有攻击都依赖引号​​:并非所有的注入攻击都依赖于滥用引号字符。例如,当 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 语句​​(如 CREATEALTERDROP),仅允许执行 DML(SELECTINSERTUPDATEDELETE)。数据表结构变更应在发布窗口使用临时高权限账号执行。
  • ​精细化权限​​:对于复杂系统:
    • 限制仅能通过​​存储过程​​修改数据。
    • 对查询服务使用​​只读账号​​。
  • ​纵深防御效果​​:即便攻击者入侵系统,也能​​最大限度限制其破坏范围​​。
​密码哈希存储 (Password Hashing)​
  • ​绝对避免明文存储密码​​,此为重大安全隐患。
  • 必须使用​​强单向哈希算法​​(如 Argon2, bcrypt, scrypt)处理密码。
  • ​务必加盐 (Salted)​​ 以防范彩虹表攻击。
  • ​安全价值​​:即使凭据泄露,攻击者也难以还原原始密码或冒用用户身份。
​第三方认证集成 (Third-Party Authentication)​
  • ​考虑外包认证流程​​:利用Google等提供的 ​​OAuth API​​。
  • ​核心优势​​:
    • ​开发便捷​​:避免自行实现认证系统。
    • ​用户信任​​:用户使用现有平台账号登录,密码​​仅存储于该平台​​(单一保管点)。
    • ​安全提升​​:依托大厂成熟、持续维护的安全基础设施。

​总结要点:​

  1. ​参数化语句(Prepared Statements)是黄金准则​​,在所有语言/库中优先使用。
  2. ​ORM 框架能自动防御注入​​,但需避免在其内部​​手动拼接 SQL​​。
  3. ​最小权限原则​​为第二道防线,显著降低入侵后的破坏力。
  4. ​密码必须加盐哈希存储​​,彻底杜绝明文密码风险。
  5. ​第三方认证​​是兼顾安全与用户体验的优选方案。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值