如何避免出現(xiàn)SQL注入漏洞
一、前言
本文將針對(duì)開發(fā)過程中依舊經(jīng)常出現(xiàn)的SQL編碼缺陷,講解其背后原理及形成原因。并以幾個(gè)常見漏洞存在形式,提醒技術(shù)同學(xué)注意相關(guān)問題。最后會(huì)根據(jù)原理,提供解決或緩解方案。
二、SQL注入漏洞的原理、形成原因
SQL注入漏洞,根本上講,是由于錯(cuò)把外部輸入當(dāng)作SQL代碼去執(zhí)行。目前最佳的解決方案就是預(yù)編譯的方式。
SQL語(yǔ)句在執(zhí)行過程中,需要經(jīng)過以下三大基本步驟:
- 代碼語(yǔ)義分析
- 制定執(zhí)行計(jì)劃
- 獲得返回結(jié)果
而一個(gè)SQL語(yǔ)句是由代碼和數(shù)據(jù)兩部分,如:
- SELECT id, name, phone FROM userTable WHERE name = 'xiaoming';
SELECT id, name, phone FROM userTable WHERE name = 是代碼,'xiaoming'是數(shù)據(jù)。
而預(yù)編譯,以Mybatis為例,就是預(yù)先分析帶有占位符的語(yǔ)義:
如SELECT id, name, phone FROM userTable WHERE id = #{name};
然后再將數(shù)據(jù)'xiaoming',傳入到占位符。這樣一來,錯(cuò)開來代碼語(yǔ)義分析階段,也就不會(huì)被誤認(rèn)為是代碼的一部分了。
在最早期,開發(fā)者顯式使用JDBC來自己創(chuàng)建Connection,執(zhí)行SQL語(yǔ)句。這種情況下,如果將外部可控?cái)?shù)據(jù)拼接到SQL語(yǔ)句,且沒有做充分過濾的話,就會(huì)產(chǎn)生漏洞。這種情況在正常的業(yè)務(wù)開發(fā)過程中已經(jīng)很少了,按照公司規(guī)定,無特殊情況下,必須使用ORM框架來執(zhí)行SQL。
但目前部分項(xiàng)目中,仍會(huì)使用JDBC來編寫一些工具腳本,如DataMerge.java 、DatabaseClean.java,借用JDBC的靈活性,通過這些腳本來執(zhí)行數(shù)據(jù)庫(kù)批量操作。
此類代碼不應(yīng)該出現(xiàn)在線上版本中,以免因各種情況,被外部調(diào)用。
三、直接使用Mybatis
1. 易錯(cuò)點(diǎn)
目前大部分的平臺(tái)代碼是基于Mybatis來處理持久層和數(shù)據(jù)庫(kù)之間的交互的,Mybatis傳入數(shù)據(jù)有兩種占位符{}和#{}。{}和#{}。{}可以理解為語(yǔ)義分析前的字符串拼接,講傳入的參數(shù),原封不動(dòng)地傳入。
比如說
- SELECT id, name, phone FROM userTable WHERE name = '${name}';
傳入name=xiaoming后,相當(dāng)于
- SELECT id, name, phone FROM userTable WHERE name = 'xiaoming';
實(shí)際應(yīng)用中
- SELECT id, name, phone FROM userTable WHERE ${col} = 'xiaoming';
傳入col = "name",相當(dāng)于
- SELECT id, name, phone FROM userTable WHERE name = 'xiaoming';
就像預(yù)編譯原理介紹里講的一樣,使用#{} 占位符就不存在注入問題了。但有些業(yè)務(wù)場(chǎng)景是不可以直接使用#{}的。
(1) 比如order by語(yǔ)法中
如果編寫SELECT id, name, phone FROM userTable ORDER BY #{}; ,執(zhí)行時(shí)是會(huì)報(bào)錯(cuò)的。因?yàn)閛rder by后的內(nèi)容,是一個(gè)列名,屬于代碼語(yǔ)義的一部分。如果在語(yǔ)義分析部分沒有確定下來,就相當(dāng)于執(zhí)行SELECT id, name, phone FROM userTable ORDER BY ??隙〞?huì)有語(yǔ)法錯(cuò)誤。
(2) 再比如like場(chǎng)景下
- SELECT id, name, phone FROM userTable WHERE name like '%#{name}%';
#{}不會(huì)被解析,從而導(dǎo)致報(bào)錯(cuò)。
in 語(yǔ)法和 between語(yǔ)法都是如此,那么如何解決這類問題呢?
2. 正確寫法
(1) order by(group by)語(yǔ)句中使用${}
使用條件判斷
- <select id="getUserAndOrder" resultType="Emp" parameterType="Emp">
- select * from users where id < #{id}
- <choose>
- <when test="order == \"name\"">
- order by name
- </when>
- <when test="order != \"age\"">
- order by age
- </when>
- <otherwise>
- order by id
- </otherwise>
- </choose>
- </select>
使用全局過濾機(jī)制,限制order by后的變量?jī)?nèi)容只能是數(shù)字、字母、下劃線。
如使用正則過濾:
- keywordkeyword = keyword.replaceAll("[^a-zA-Z0-9_\s+]", "");
這里需要注意,過濾需要使用白名單,不能使用黑名單,黑名單無法解決注入問題。
(2) LIKE語(yǔ)句
由于需要like中的關(guān)鍵詞需要包裹在兩個(gè)%符號(hào)中,因此可以使用CONCAT函數(shù)進(jìn)行拼接。
- <select id="selectStudentByFuzzyQuery" resultMap="studentMap">
- SELECT *
- FROM student
- WHERE student.stu_name
- LIKE CONCAT('%',#{stuName},'%')
- </select>
注意不要用 CONCAT('%','${stuName}','%') ,這樣仍然存在漏洞。也就是說,使用$符號(hào)是不對(duì)的,使用#符號(hào)才安全。
(3) IN語(yǔ)句
類似于like語(yǔ)句,直接使用#{}會(huì)報(bào)錯(cuò),常見的錯(cuò)誤寫法為:
- tenant_id in (${tenantIds})
正確的寫法為:
- select * from news where id in
- <foreach collection="ids" item="item" open="("separator="," close=")">#{item}</foreach>
四、Mybatis-generator使用安全
繁重的CRUD代碼壓力下,開發(fā)者慢慢開始通過Mybatis-generator、idea-mybatis-generator插件、通用Mapper、Mybatis-generator-plus來自動(dòng)生成Mapper、POJO、Dao等文件。
這些工具可以自動(dòng)的生成CRUD所需要的文件,但如果使用不當(dāng),就會(huì)自動(dòng)產(chǎn)生SQL注入漏洞。我們以最常用的org.mybatis.generator為例,來講解可能會(huì)出現(xiàn)的問題。
1. 動(dòng)態(tài)語(yǔ)句支持
Mybatis-generator提供來一些函數(shù),幫助用戶把SQL的各個(gè)條件連接起來,比如多個(gè)參數(shù)的like語(yǔ)法,多個(gè)參數(shù)的比較語(yǔ)法。為了保證使用的簡(jiǎn)潔性,需要使用將一些語(yǔ)義代碼拼接到SQL語(yǔ)句中。而如果開發(fā)者使用不當(dāng),將外部輸入也傳入了{(lán)}占位符。就會(huì)產(chǎn)生漏洞。
2. targetRuntime參數(shù)配置
在配置generator時(shí),配置文件generator-rds.xml中有一個(gè)targetRuntime屬性,默認(rèn)為MyBatis3。在這種情況下,會(huì)啟動(dòng)Mybatis的動(dòng)態(tài)語(yǔ)句支持,啟動(dòng)enableSelectByExample、enableDeleteByExample、enableCountByExample 以及 enableUpdateByExample功能。
以enableSelectByExample為例,會(huì)在xml映射文件中代入以下動(dòng)態(tài)模塊:
- <sql id="Example_Where_Clause" >
- <where >
- <foreach collection="oredCriteria" item="criteria" separator="or" >
- <if test="criteria.valid" >
- <trim prefix="(" suffix=")" prefixOverrides="and" >
- <foreach collection="criteria.criteria" item="criterion" >
- <choose >
- <when test="criterion.noValue" >
- and ${criterion.condition}
- </when>
- <when test="criterion.singleValue" >
- and ${criterion.condition} #{criterion.value}
- </when>
- <when test="criterion.betweenValue" >
- and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
- </when>
- <when test="criterion.listValue" >
- and ${criterion.condition}
- <foreach collection="criterion.value" item="listItem" open="(" close=")" separator="," >
- #{listItem}
- </foreach>
- </when>
- </choose>
- </foreach>
- </trim>
- </if>
- </foreach>
- </where>
- </sql>
開發(fā)者include該模塊就可以添加where條件,但如果使用不當(dāng),就會(huì)導(dǎo)致SQL注入漏洞:
- <select id="selectByExample" resultMap="BaseResultMap" parameterType="com.doctor.mybatisdemo.domain.userExample" >
- select
- <if test="distinct" >
- distinct
- </if>
- <include refid="Base_Column_List" />
- from user
- <if test="_parameter != null" >
- <include refid="Example_Where_Clause" />
- </if>
- <if test="orderByClause != null" >
- order by ${orderByClause}
- </if>
- </select>
并使用自定義的參數(shù)添加函數(shù):
- public Criteria addKeywordTo(String keyword) {
- StringBuilder sb = new StringBuilder();
- sb.append("(display_name like '%" + keyword + "%' or ");
- sb.append("org like '" + keyword + "%' or ");
- sb.append("status like '%" + keyword + "%' or ");
- sb.append("id like '" + keyword + "%') ");
- addCriterion(sb.toString());
- return (Criteria) this;
- }
目的是為了實(shí)現(xiàn)同時(shí)對(duì)display_name、org、status、id的like操作。其中addCriterion是Mybatis-generator自帶的函數(shù):
- protected void addCriterion(String condition) {
- if (condition == null) {
- throw new RuntimeException("Value for condition cannot be null");
- }
- criteria.add(new Criterion(condition));
- }
這里的誤區(qū)在于,addCriterion本身提供了多個(gè)條件的支持,但開發(fā)者認(rèn)為需要自己把多個(gè)條件拼接起來,一同傳入addCriterion方法。如同案例中的代碼一樣,最終傳入addCriterion的只有一個(gè)參數(shù)。從而執(zhí)行Example_Where_Clause語(yǔ)句中的:
- <when test="criterion.noValue" >
- and ${criterion.condition}
- </when>
也就是說,開發(fā)者把自己拼接的SQL語(yǔ)句,直接代入了${criterion.condition}中,從而導(dǎo)致了漏洞的產(chǎn)生。
而按照Mybatis-generator的文檔,正確的寫法應(yīng)該是:
- public void addKeywordTo(String keyword, UserExample userExample) {
- userExample.or().andDisplayNameLike("%" + keyword + "%");
- userExample.or().andOrgLike(keyword + "%");
- userExample.or().andStatusLike("%" + keyword + "%");
- userExample.or().andIdLike("%" + keyword + "%");
- }
or方法負(fù)責(zé)創(chuàng)建Criteria,這時(shí)觸發(fā)的邏輯就是
- <when test="criterion.singleValue" >
- and ${criterion.condition} #{criterion.value}
- </when>
${criterion.condition}被替換為了沒有單引號(hào)的like,like作為語(yǔ)義代碼,在語(yǔ)義分析前拼接到了SQL語(yǔ)句中,而"%" + keyword + "%"會(huì)作為數(shù)據(jù)添加到預(yù)編譯#{criterion.value}中去,從而避免了注入。
類似的,也提供了In語(yǔ)法的安全使用方法:
- List<Integer> field5Values = new ArrayList<Integer>();
- field5Values.add(8);
- field5Values.add(11);
- field5Values.add(14);
- field5Values.add(22);
- example.or()
- .andField5In(field5Values);
Beetween的安全使用方法:
- example.or()
- .andField6Between(3, 7);
Mybatis-generator默認(rèn)生成的order by語(yǔ)句也是使用${}直接進(jìn)行拼接的:
- <if test="orderByClause != null" >
- order by ${orderByClause}
- </if>
如果沒有對(duì)傳入的參數(shù)進(jìn)行額外的過濾的話,就會(huì)導(dǎo)致注入問題。
3. order by
除了自己寫的SQL語(yǔ)句以外,Mybatis-generator默認(rèn)生成的order by語(yǔ)句也是使用${}直接進(jìn)行拼接的:
- <if test="orderByClause != null" >
- order by ${orderByClause}
- </if>
如果沒有對(duì)傳入的參數(shù)進(jìn)行額外的過濾的話,就會(huì)導(dǎo)致注入問題。
PS: 實(shí)際掃雷過程中發(fā)現(xiàn)很多語(yǔ)句自動(dòng)生成了order by語(yǔ)法,但上層調(diào)用時(shí),并沒有傳入該可選參數(shù)。這種情況應(yīng)當(dāng)刪除多余的order by語(yǔ)法。
4. 其它插件
插件與插件之間的安全缺陷還不太一樣,下面簡(jiǎn)單列舉了常用的幾種插件。
(1) idea-mybatis-generator
這是IDEA的插件,可以在開發(fā)過程中,從IDE的層面,自動(dòng)生成CRUD中需要的文件。使用該插件時(shí),也有一些默認(rèn)安全隱患需要注意。
1)自定義order by處理
like\in\between可以參照官方文檔使用,無安全隱患。
但該插件沒有內(nèi)置的order by處理,需要自行編寫,編寫時(shí),參考Case2
2)默認(rèn)的IF條件前需要判斷是否為空
插件默認(rèn)生成的語(yǔ)法大致如下:
- <if test="ID != null">
- ID = #{ID} and
當(dāng)ID參數(shù)為null時(shí),if標(biāo)簽下的邏輯不會(huì)添加到SQL語(yǔ)句中,可能會(huì)導(dǎo)致DOS、權(quán)限繞過等漏洞。因此,參數(shù)傳入查詢語(yǔ)句前,需要確認(rèn)不為空。
(2) com.baomidou.mybatis-plus
- apply方法傳參時(shí),應(yīng)當(dāng)使用{}
- 自帶的last方法,其原理是直接拼接到SQL語(yǔ)句的末尾,存在注入漏洞。
五、其它ORM框架
1. Hibernate
ORM全稱為對(duì)象關(guān)系映射(Object Relational Mapping),簡(jiǎn)單地說,就是將數(shù)據(jù)庫(kù)中的表映射為Java對(duì)象, 這種只有屬性,沒有業(yè)務(wù)邏輯的對(duì)象也叫做POJO(Plain Ordinary Java Object)對(duì)象。
Hibernate是第一個(gè)被廣泛使用的ORM框架,它通過XML管理數(shù)據(jù)庫(kù)連接,提供全表映射模型,封裝程度很高。在配置映射文件和數(shù)據(jù)庫(kù)鏈接文件后,Hibernate就可以通過Session對(duì)象進(jìn)行數(shù)據(jù)庫(kù)操作,開發(fā)者無需接觸SQL語(yǔ)句,只需要寫HQL語(yǔ)句即可。
Hibernate經(jīng)常與Struts、Spring搭配使用,也就是Java世界的經(jīng)典SSH框架。
HQL相較于SQL,多了很多語(yǔ)法限制:
- 不能查詢未做映射的表,只有當(dāng)模型之間的關(guān)系明確后,才可以使用UNION語(yǔ)法。
- 表名,列名大小寫敏感。
- 沒有*、#、-- 。
- 沒有延時(shí)函數(shù)。
所以HQL注入利用要比SQL注入苦難得多。從代碼審計(jì)的角度和普通SQL注入是一致的:
拼接會(huì)導(dǎo)致注入漏洞:
- List<Student> studentList = session.createQuery("FROM Student s WHERE s.stuId = " + stuId).list();
可以使用占位符和具名參數(shù)來防止SQL語(yǔ)句,其本質(zhì)都是預(yù)編譯。
- List<Student> studentList = session.createQuery("FROM Student s WHERE s.stuId = :stuId").setParameter("stuId",stuId).list();
- List<Student> studentList = session.createQuery("FROM Student s WHERE s.stuId = ?").setParameter(stuId).list();
Hibernate在使用過程中有很多不足:
- 全表映射不靈活,更新時(shí)需要發(fā)送所有字段,影響程序運(yùn)行效率。
- 對(duì)復(fù)雜查詢的支持很差。
- 對(duì)存儲(chǔ)過程的支持很差。
- HQL性能較差,無法根據(jù)SQL進(jìn)行優(yōu)化。
在審計(jì)Hibernate相關(guān)注入時(shí),可以通過全局搜索createQuery來快速定位SQL操作的位置。
2. JPA
JPA全稱為Java Persistence API,是Java EE提供的一種數(shù)據(jù)持久化的規(guī)范,允許開發(fā)者通過XML或注解的方式,將某個(gè)對(duì)象,持久化到數(shù)據(jù)庫(kù)中。
主要包括三方面內(nèi)容:
(1) ORM映射元數(shù)據(jù),通過XML或注解,描述對(duì)象和數(shù)據(jù)表之間的對(duì)應(yīng)關(guān)系??蚣鼙憧梢宰詣?dòng)將對(duì)象中的數(shù)據(jù)保存到數(shù)據(jù)庫(kù)中。
常見的注解有:@Entity、@Table、@Column、@Transient
(2) 數(shù)據(jù)操作API,內(nèi)置接口,方便對(duì)某個(gè)數(shù)據(jù)表執(zhí)行CRUD操作,節(jié)省開發(fā)者編寫SQL的時(shí)間。
常見的方法有:entityManager.merge(T t);
(3) JPQL, 提供一種面向?qū)ο蠖皇敲嫦驍?shù)據(jù)庫(kù)的查詢語(yǔ)言,將程序和數(shù)據(jù)庫(kù)、SQL解耦合。
JPA是一套規(guī)范,Hibernate實(shí)現(xiàn)了這一JPA規(guī)范。
在Spring框架中,提供了簡(jiǎn)易版的JPA實(shí)現(xiàn)——spirng data jpa。按照約定好的方法命名規(guī)則寫dao層接口,就可以在不寫接口實(shí)現(xiàn)的情況下,實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的訪問和操作。同時(shí)提供了很多除了CRUD之外的功能,如分頁(yè)、排序、復(fù)雜查詢等等。使用起來更簡(jiǎn)單,但底層仍然在使用Hibernate的JPA實(shí)現(xiàn)。
和HQL注入一樣,如果使用拼接的方式,將用戶可控的數(shù)據(jù)代入了查詢語(yǔ)句中,就會(huì)導(dǎo)致SQL注入。
安全的查詢應(yīng)該使用預(yù)編譯技術(shù)。
Spring Data JPA的預(yù)編譯寫法為:
- String getUser = "SELECT username FROM users WHERE id = ?";
- Query query = em.createNativeQuery(getUser);
- query.setParameter(1, id);
- String username = query.getResultList();
小貼士:其實(shí)Hibernate的出現(xiàn)日期比JPA規(guī)范要早,Hibernate逐漸成熟之后,JavaEE的開發(fā)團(tuán)隊(duì),邀請(qǐng)Hibernate核心開發(fā)人員一起制定了JPA規(guī)范。之后Spring Data JPA按照規(guī)范做了進(jìn)一步優(yōu)化。除此之外,JPA規(guī)范的實(shí)現(xiàn)有很多產(chǎn)品,比如Eclipse的TopLink(OracleLink)。
六、 總結(jié)
經(jīng)過上面的介紹,尤其是圍繞Mybatis易錯(cuò)點(diǎn)的討論,我們可以得到以下結(jié)論:
- 持久層組件種類繁多。
- 開發(fā)者對(duì)工具使用的錯(cuò)誤理解,是漏洞出現(xiàn)的主要原因。
- 由于自動(dòng)生成插件的動(dòng)態(tài)特性,自動(dòng)化發(fā)現(xiàn)SQL漏洞不能簡(jiǎn)單地使用${}來尋找。必須要根據(jù)全局的持久層組件特性,來做詳細(xì)的匹配規(guī)則。
【本文為51CTO專欄作者“阿里巴巴官方技術(shù)”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】