fastjson:差點被幾個漏洞毀了一世英名

01、前世今生

我是 fastjson,是個地地道道的杭州土著,但我始終懷揣着一顆走向全世界的雄心。這不,我在 GitHub 上的簡介都換成了英文,國際範十足吧?

如果你的英語功底沒有我家老闆 666 的話,我可以簡單地翻譯下(說人話,不裝逼)。

我是阿里巴巴開源的一款 JSON 解析庫,可以將 Java 對象序列化成 JSON 字符串,同時也可以將 JSON 字符串反序列化爲 Java 對象。

  • 我提供了服務器端和安卓客戶端兩種解析工具,性能表現還不錯。

  • 我提供了便捷的方式來進行 Java 對象和 JSON 之間的互轉,toJSONString() 方法用來序列化,parseObject() 方法用來反序列化。

  • 我允許轉換預先存在的無法修改的對象(只有 class、沒有源代碼)。

  • 對 Java 泛型有着廣泛的支持。

  • 我支持任意複雜的對象(深度的繼承層次)。

2012 年的時候,我就被開源中國評選爲最受歡迎的國產開源軟件之一。時隔多年,我的流行趨勢沒有絲毫減退,在 JSON 領域,我敢說我是 NO 1,因爲我在 GitHub 上的粉絲數已經超過了 22k,沒有任何人敢忽視我這樣的成就。

02、使用指南

在使用我的 API 之前,需要先在 pom.xml 文件中引入我的依賴。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.58</version>
</dependency>

我來寫一個簡單的測試用例,你看一下。

public class Test {
    public static void main(String[] args) {
        Writer writer = new Writer();
        writer.setAge(18);
        writer.setName("沉默王二");

        String json = JSON.toJSONString(writer);
        System.out.println(json);
    }
}
class Writer {
    private int age;
    private String name;

    // getter/setter
}

Writer 是一個普通的 Java 類,有兩個字段,分別是 age 和 name,還有它們倆對應的 getter 和 setter 方法。

main() 方法中創建了一個 Writer 對象,然後調用我提供的一個靜態方法 JSON.toJSONString() 來得到 JSON 字符串。

來看一下打印後的結果。

{"age":18,"name":"沉默王二"}

如果想反序列化的話,執行以下的代碼即可。

Writer writer1 = JSON.parseObject(json, Writer.class);

調用靜態方法 JSON.parseObject(),傳遞兩個參數,一個是 JSON 字符串,一個是對象的類型。

如果想把 JSON 字符串轉成集合的話,需要調用另外一個靜態方法 JSON.parseArray()

List<Writer> list = JSON.parseArray("[{\"age\":18,\"name\":\"沉默王二\"},{\"age\":19,\"name\":\"沉默王一\"}]", Writer.class);

如果沒有特殊要求的話,我敢這麼說,以上 3 個方法就可以覆蓋到你絕大多數的業務場景了。

03、使用註解

有時候,你的 JSON 字符串中的 key 可能與 Java 對象中的字段不匹配,比如大小寫;有時候,你需要指定一些字段序列化但不反序列化;有時候,你需要日期字段顯示成指定的格式。

這些特殊場景,我統統爲你考慮到了,只需要在對應的字段上加上 @JSONField 註解就可以了。

先來看一下 @JSONField 註解的定義吧。

public @interface JSONField {
    String name() default "";
    String format() default "";
    boolean serialize() default true;
    boolean deserialize() default true;
}

name 用來指定字段的名稱,format 用來指定日期格式,serialize 和 deserialize 用來指定是否序列化和反序列化。

class Writer {
    private int age;
    private String name;
    private Date birthday;

    @JSONField(format = "yyyy年MM月dd日")
    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @JSONField(name = "Age")
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @JSONField(serialize = false,deserialize = true)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

我建議在 getter 字段上使用 @JSONField 註解。來看一下測試代碼。

Writer writer = new Writer();
writer.setAge(18);
writer.setName("沉默王二");
writer.setBirthday(new Date());

String json = JSON.toJSONString(writer);
System.out.println(json);

此時的輸出結果如下所示。

{"Age":18,"birthday":"2020年12月17日"}

JSON 字符串中的 Age 首字母爲大寫,birthday 的格式符合“年月日”的預期,name 字段沒有出現在結果中,說明沒有被序列化。

04、序列化特性

爲了滿足更多個性化的需求,我在 SerializerFeature 類中定義了很多特性,你可以在調用 toJSONString() 方法的時候進行指定。

  • PrettyFormat,讓 JSON 格式打印得更漂亮一些
  • WriteClassName,輸出類名
  • UseSingleQuotes,key 使用單引號
  • WriteNullListAsEmpty,List 爲空則輸出 []
  • WriteNullStringAsEmpty,String 爲空則輸出“”

等等等等,更多新技能,等待你去開鎖。我這裏寫個簡單的 demo 供你參考。

System.out.println(JSON.toJSONString(writer, 
SerializerFeature.PrettyFormat, 
SerializerFeature.UseSingleQuotes));

對比一下配置前和配置後的結果。

{"Age":18,"birthday":"2020年12月17日"}
{
    'Age':18,
    'birthday':'2020年12月17日'
}

05、我爲什麼快

衆所周知,把 Java 對象序列化成 JSON 字符串,是不可能使用字符串直接拼接的,因爲這樣性能很差。比字符串拼接更好的辦法就是使用 StringBuilder

StringBuilder 儘管已經很好了,但在性能上還有上升的空間。“自己動手,豐衣足食”,於是我就創造了一個 SerializeWriter 類,專門用來序列化。

SerializeWriter 類中包含了一個 char[] buf,每序列化一次,都要做一次分配,但我使用了 ThreadLocal 來進行優化,這樣就能夠有效地減少對象的分配和垃圾回收,從而提升性能。

private final static ThreadLocal<char[]> bufLocal         = new ThreadLocal<char[]>();

public SerializeWriter(java.io.Writer writer, int defaultFeatures, SerializerFeature... features){
    this.writer = writer;

    buf = bufLocal.get();

    if (buf != null) {
        bufLocal.set(null);
    } else {
        buf = new char[2048];
    }
}

除此之外,還有很多其他的細節,比如說使用 IdentityHashMap 而不是 HashMap,既可以避免多餘的 equals 操作,又可以避免多線程併發情況下的死循環。

/**
 * for concurrent IdentityHashMap
 * 
 * @author wenshao[[email protected]]
 */
@SuppressWarnings("unchecked")
public class IdentityHashMap<K, V> {
    private final Entry<K, V>[] buckets;
    private final int           indexMask;
    public final static int DEFAULT_SIZE = 8192;
}

再比如說,使用 asm 技術來避免反射導致的開銷。

我承認,快的同時,也帶來了一些安全性的問題。尤其是 AutoType 的引入,讓黑客有了可乘之機。

1.2.59 發佈,增強 AutoType 打開時的安全性

1.2.60 發佈,增加了 AutoType 黑名單,修復拒絕服務安全問題

1.2.61 發佈,增加 AutoType 安全黑名單

1.2.62 發佈,增加 AutoType 黑名單、增強日期反序列化和 JSONPath

1.2.66 發佈,Bug 修復安全加固,並且做安全加固,補充了 AutoType 黑名單

1.2.67 發佈,Bug 修復安全加固,補充了 AutoType 黑名單

1.2.68 發佈,支持 GEOJSON,補充了 AutoType 黑名單。(引入一個 safeMode 的配置,配置 safeMode 後,無論白名單和黑名單,都不支持 autoType。)

1.2.69 發佈,修復新發現高危 AutoType 開關繞過安全漏洞,補充了 AutoType 黑名單

1.2.70 發佈,提升兼容性,補充了 AutoType 黑名單

在於黑客的反覆較量中,我雖然變得越來越穩重成熟了,但與此同時,讓我的用戶爲此也付出了沉重的代價。

網絡上也出現了很多不和諧的聲音,他們聲稱我是最垃圾的國產開源軟件之一,只不過憑藉着一些投機取巧贏得了國內開發者的信賴。

但更多的是,對我的不離不棄。

最令我感到爲之動容的一句話是:

溫少幾乎憑一己之力撐起了一個被廣泛使用 JSON 庫,而其他庫幾乎都是靠一整個團隊,就憑這一點,溫少作爲“初心不改的阿里初代開源人”,當之無愧。

出現漏洞並不可怕,可怕的是發現不了漏洞,或者說無法解決掉漏洞。

爲了徹底解決 AutoType 帶來的問題,在 1.2.68 版本中,我引入了 safeMode 的安全模式,無論白名單和黑名單,都不支持 AutoType,這樣就可以徹底地杜絕攻擊。

安全模式下,checkAutoType() 方法會直接拋出異常。

06、尾聲

不管前面的路還有多少艱難困苦,也不管還要面對多少風言風語,我都會砥礪前行,爲了國產開源軟件的蓬勃發展,我願意做一個先驅者,也願意做一個持久戰者。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章