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'永远为真。整个查询就返回了所有用户。攻击者就这么登录成功了。
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);
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
看到了吗?直接用参数,不要用EXEC或sp_executesql去拼字符串。
| 方式 | 是否安全 | 说明 |
|---|---|---|
| 参数化查询 | ✅ 安全 | SQL与数据分离,推荐首选 |
| ORM(正确使用) | ✅ 安全 | 使用参数占位符,避免拼接 |
| 存储过程(正确使用) | ✅ 安全 | 预编译,参数化调用 |
| 字符串拼接 | ❌ 危险 | 无论在哪拼接,都有注入风险 |
3.5 避坑指南
我总结了几条实战经验,分享给你:
- 不要相信任何用户输入。不管是表单、URL参数、HTTP头,还是从文件读取的数据,都当成不可信的。
- 能用参数化,就别想别的。这是最省心、最彻底的方案。
- ORM的
${}要慎用。只有动态表名、字段名这种不得已的场景才用,而且要对输入做严格校验。 - 存储过程里别用
EXEC拼SQL。我见过太多人觉得「存过里安全」,结果翻车了。 - 定期做安全扫描。用工具跑一遍,很多注入点自己就暴露了。
好了,SQL注入这块,说白了就一句话:别让用户输入变成SQL代码。做到这一点,你就防住了90%的注入攻击。