3、SQL注入防御:SQL注入原理、参数化查询、ORM安全使用、存储过程安全

3.1 SQL注入到底是怎么回事?

说实话,SQL注入是我在安全培训里最常讲的一个漏洞。为什么?因为它太经典了,也太容易犯了。

简单来说,SQL注入就是攻击者把恶意的SQL代码,通过输入框塞进了你的数据库查询里。你想想看,如果你直接把用户输入拼接到SQL语句里,那用户输入的内容就成了SQL的一部分。

举个例子,一个登录页面,你可能会写这样的代码:

String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

看起来没问题对吧?但假如用户在用户名里输入了:

' OR '1'='1

那拼接出来的SQL就变成了:

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

嗯,'1'='1'永远为真。整个查询就返回了所有用户。攻击者就这么登录成功了。

⚠️ 注意: 我见过不少团队,觉得只要过滤几个特殊字符就安全了。其实不然。攻击者可以绕过很多过滤方式。真正靠谱的,是彻底不让用户输入参与SQL拼接。

3.2 参数化查询——最直接的防御

我个人习惯,只要涉及数据库操作,一律用参数化查询。说白了,就是把SQL语句和用户数据分开处理。

拿Java的PreparedStatement来说:

String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

看到了吗??是占位符。用户输入再奇怪,也只是被当作字符串值,不会变成SQL指令。

我在项目中遇到过一件事。有个同事觉得参数化查询太麻烦,坚持用拼接。结果上线第二天,就被安全扫描工具打回来了。嗯,从那以后,他比谁都积极用参数化。

其他语言也一样:

  • Python(MySQLdb)cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
  • C#(ADO.NET)SqlCommand cmd = new SqlCommand("SELECT * FROM users WHERE name = @name", conn); cmd.Parameters.AddWithValue("@name", name);
  • Node.js(mysql2)connection.execute('SELECT * FROM users WHERE name = ?', [name])
💡 小技巧: 参数化查询不仅防注入,还能提升性能。因为数据库可以缓存执行计划,重复查询时更快。

3.3 ORM安全使用——别让框架坑了你

ORM框架,比如Hibernate、MyBatis、Entity Framework,用起来确实方便。但你要是用错了,照样有注入风险。

我记得有一次,一个团队用MyBatis,写了个这样的查询:

<select id="findUser" resultType="User">
  SELECT * FROM users WHERE name LIKE '%${name}%'
</select>

这里用了${},是直接拼接字符串。攻击者输入' OR 1=1 --,又完蛋了。

正确的做法是用#{}

<select id="findUser" resultType="User">
  SELECT * FROM users WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

为什么?#{}会生成参数化查询,而${}是直接替换。记住一个原则:能用#{}的地方,绝不用${}

Hibernate也一样。如果你用HQL拼接字符串,比如:

String hql = "from User where name = '" + name + "'";

那跟裸写SQL拼接没区别。正确做法是用命名参数:

Query query = session.createQuery("from User where name = :name");
query.setParameter("name", name);
✅ 核心原则: ORM框架只是工具,不是护身符。你写的是参数化查询,它就安全;你写的是拼接,它就不安全。

3.4 存储过程——安全但别大意

存储过程本身是安全的,因为它在数据库端预编译。但前提是——你不在存储过程里拼接SQL。

我曾经见过一个存储过程,里面写了这样的代码:

CREATE PROCEDURE GetUser
    @username NVARCHAR(50)
AS
BEGIN
    DECLARE @sql NVARCHAR(MAX)
    SET @sql = 'SELECT * FROM users WHERE username = ''' + @username + ''''
    EXEC(@sql)
END

这叫什么?这叫「换了个地方拼接」。攻击者照样可以注入。

正确的存储过程应该是:

CREATE PROCEDURE GetUser
    @username NVARCHAR(50)
AS
BEGIN
    SELECT * FROM users WHERE username = @username
END

看到了吗?直接用参数,不要用EXECsp_executesql去拼字符串。

方式 是否安全 说明
参数化查询 ✅ 安全 SQL与数据分离,推荐首选
ORM(正确使用) ✅ 安全 使用参数占位符,避免拼接
存储过程(正确使用) ✅ 安全 预编译,参数化调用
字符串拼接 ❌ 危险 无论在哪拼接,都有注入风险

3.5 避坑指南

我总结了几条实战经验,分享给你:

  • 不要相信任何用户输入。不管是表单、URL参数、HTTP头,还是从文件读取的数据,都当成不可信的。
  • 能用参数化,就别想别的。这是最省心、最彻底的方案。
  • ORM的${}要慎用。只有动态表名、字段名这种不得已的场景才用,而且要对输入做严格校验。
  • 存储过程里别用EXEC拼SQL。我见过太多人觉得「存过里安全」,结果翻车了。
  • 定期做安全扫描。用工具跑一遍,很多注入点自己就暴露了。
⚠️ 我曾经踩过的坑: 有一次,我觉得「反正用户只能选下拉框的值,不会出问题」。结果有人抓包改了请求参数,直接注入了。从那以后,我连下拉框的值都做参数化处理。

好了,SQL注入这块,说白了就一句话:别让用户输入变成SQL代码。做到这一点,你就防住了90%的注入攻击。