Java 正則表達式的災難性回溯

原文地址:https://alphahinex.github.io/2024/10/13/java-regex-catastrophic-backtracking/


description: "Java 正則表達式的災難性回溯問題可能導致應用程序的拒絕服務。本文介紹了如何重現(xiàn)蔫仙、風險原因游岳、風險評估掰烟、如何避免锁蠕、修復示例等內容统求。"
date: 2024.10.13 10:26
categories:
- Java
tags: [Java, Regex]
keywords: regex, backtracking, catastrophic backtracking, java, security, performance, denial of service, DoS, ReDoS, OWASP, CWE, S5852


現(xiàn)象重現(xiàn)

新建一個 Backtracking.java 文件池磁,內容如下:

public class Backtracking {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.version"));
        System.out.println("The first regex evaluation will never end in JDK <= 9:");
        System.out.println(java.util.regex.Pattern.compile("(a+)+").matcher(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+
"aaaaaaaaaaaaaaa!").matches()); // Sensitive
        System.out.println("and the second regex evaluation will never end in any versions of the JDK:");
        System.out.println(java.util.regex.Pattern.compile("(h|h|ih(((i|a|c|c|a|i|i|j|b|a|i|b|a|a|j))+h)ahbfhba|c|i)*").matcher(
"hchcchicihcchciiicichhcichcihcchiihichiciiiihhcchi"+
"cchhcihchcihiihciichhccciccichcichiihcchcihhicchcciicchcccihiiihhihihihi"+
"chicihhcciccchihhhcchichchciihiicihciihcccciciccicciiiiiiiiicihhhiiiihchccch"+
"chhhhiiihchihcccchhhiiiiiiiicicichicihcciciihichhhhchihciiihhiccccccciciihh"+
"ichiccchhicchicihihccichicciihcichccihhiciccccccccichhhhihihhcchchihih"+
"iihhihihihicichihiiiihhhhihhhchhichiicihhiiiiihchccccchichci").matches()); // Sensitive
    }
}

編譯并執(zhí)行:

$ java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
$ javac Backtracking.java
$ java Backtracking
1.8.0_121
The first regex evaluation will never end in JDK <= 9:

在 Java 8 環(huán)境下平窘,此段程序永遠不會執(zhí)行結束,且該 Java 進程會持續(xù)跑滿 CPU跌榔。

在 Java 11 環(huán)境下异雁,第一個表達式能執(zhí)行結束,但第二個表達式依然會持續(xù)消耗大量資源:

$ java -version
openjdk version "11.0.23" 2024-04-16 LTS
OpenJDK Runtime Environment Microsoft-9394293 (build 11.0.23+9-LTS)
OpenJDK 64-Bit Server VM Microsoft-9394293 (build 11.0.23+9-LTS, mixed mode, sharing)
$ java Backtracking
11.0.23
The first regex evaluation will never end in JDK <= 9:
false
and the second regex evaluation will never end in any versions of the JDK:

風險原因

Using slow regular expressions is security-sensitive 對這個問題進行了描述:大多數(shù)正則表達式引擎使用回溯(backtracking)來嘗試正則表達式在評估輸入時的所有可能執(zhí)行路徑僧须,在某些情況下纲刀,這可能會導致性能問題,稱為災難性回溯(catastrophic backtracking)情況皆辽。在最壞的情況下芍锦,正則表達式的復雜度與輸入大小成指數(shù)關系文捶,這意味著一個精心構造的小輸入(如20個字符)可以觸發(fā)災難性回溯并導致應用程序的拒絕服務壁熄。超線性正則表達式復雜度也可能導致相同的影響敲董,在這種情況下,需要一個精心構造的大輸入(數(shù)千個字符)空另。

風險評估

要確定代碼中是否存在此風險盆耽,可以嘗試回答如下問題:

  • 輸入是用戶控制的。
  • 輸入大小沒有限制為少量的字符扼菠。
  • 沒有設置超時來限制正則表達式的評估時間摄杂。

如果任何一個問題的回答為 ,那么就可能存在風險循榆。

如何避免

在所有下述情況中析恢,災難性回溯只有在正則表達式的有問題部分后面跟隨一個可能失敗的模式時才會發(fā)生,從而導致回溯實際發(fā)生秧饮。請注意映挂,當執(zhí)行完全匹配(例如使用 String.matches)時泽篮,正則表達式的結尾被視為一個可能失敗的模式,因為它只有在到達字符串結尾時才會成功柑船。

  • 如果正則表達式包含非占有性重復帽撑,如 r*r*?,表示可以匹配零次或多次 r鞍时,但不會占有匹配的字符(即允許回溯)亏拉,如果 r 可以在相同輸入上產生不同的可能匹配(可能長度不同),最壞情況下的匹配時間可能是指數(shù)級的逆巍。這種情況可能發(fā)生在 r 包含可選部分及塘、交替或額外重復時。使用JDK 9或更高版本時蒸苇,如果重復是貪婪的且整個正則表達式不包含反向引用磷蛹,則運行時間會優(yōu)化為多項式或線性。
  • 如果多個非占有性重復可以匹配相同內容且是連續(xù)的或僅由可選分隔符分隔溪烤,可能會導致多項式時間復雜度。例如庇勃,a*b* 沒有問題檬嘀,因為 a*b* 匹配不同的東西,而 a*_a* 也沒有問題责嚷,因為重復部分由一個 _ 分隔鸳兽,并且不能匹配該 _。然而罕拂,a*a*.*_.* 具有二次運行時間揍异。
  • 如果你正在執(zhí)行部分匹配(如使用 Matcher.findString.split爆班、String.replaceAll 等)衷掷,并且正則表達式未錨定到字符串的開頭,尤其難以避免二次運行時間柿菩。例如戚嗅,str.split("\\s*,") 在完全由空格組成的字符串(或至少包含大量不跟隨逗號的空格序列)上將以二次時間運行。

為避免這些問題枢舶,可以采取以下策略:

  • 如果適用懦胞,使用有界量詞(例如用 {1,5} 代替 +)限制重復次數(shù)。
  • 重構嵌套量詞(nested quantifiers)以限制內部組可以被外部量詞匹配的數(shù)量凉泄。例如 (ba+)+ 這種嵌套量詞情況不會導致性能問題躏尉,實際上,只有存在每個組重復一次 b 字符時后众,內部組才能匹配胀糜。
  • 使用占有量詞(possessive quantifiers)和原子分組(atomic grouping)優(yōu)化正則表達式稼锅。
  • 使用否定字符類代替 . 來排除分隔符。例如僚纷,二次正則表達式 .*_.* 可以通過將其更改為 [^_]*_.* 變?yōu)榫€性矩距。

如果無法重寫正則表達式以避免性能問題,可以考慮以下方法:

  • 不使用正則表達式解決問題怖竭。
  • 使用非回溯的正則表達式實現(xiàn)锥债,如Google的 RE2RE2/J
  • 使用多次處理痊臭,預處理或后處理字符串哮肚,或使用多個正則表達式。一個例子是將 str.split("\\s*,\\s*") 替換為 str.split(",")广匙,然后在第二步中修剪字符串中的空格允趟。
  • 使用 Matcher.find() 時,通逞恢拢可以通過使所有可能失敗的部分可選來使正則表達式不可失敗潮剪,這將防止回溯。當然分唾,這意味著你將接受比預期更多的字符串抗碰,但這可以通過使用捕獲組來檢查可選部分是否匹配,然后在它們不匹配時忽略匹配來處理绽乔。例如弧蝇,正則表達式 x*y 可以替換為 x*(y)?,然后將對 matcher.find() 的調用替換為 matcher.find() && matcher.group(1) != null折砸。

修復示例

NewBacktracking.java

public class NewBacktracking {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.version"));
        System.out.println("The first regex evaluation will end after modify:");
        System.out.println(java.util.regex.Pattern.compile("(a+)++").matcher(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+
"aaaaaaaaaaaaaaa!").matches()); // Sensitive
        System.out.println("and the second regex evaluation will end after modify:");
        System.out.println(java.util.regex.Pattern.compile("(h|h|ih(((i|a|c|c|a|i|i|j|b|a|i|b|a|a|j))++h)ahbfhba|c|i)*").matcher(
"hchcchicihcchciiicichhcichcihcchiihichiciiiihhcchi"+
"cchhcihchcihiihciichhccciccichcichiihcchcihhicchcciicchcccihiiihhihihihi"+
"chicihhcciccchihhhcchichchciihiicihciihcccciciccicciiiiiiiiicihhhiiiihchccch"+
"chhhhiiihchihcccchhhiiiiiiiicicichicihcciciihichhhhchihciiihhiccccccciciihh"+
"ichiccchhicchicihihccichicciihcichccihhiciccccccccichhhhihihhcchchihih"+
"iihhihihihicichihiiiihhhhihhhchhichiicihhiiiiihchccccchichci").matches()); // Sensitive
    }
}
$ diff Backtracking.java NewBacktracking.java
1c1
< public class Backtracking {
---
> public class NewBacktracking {
4,5c4,5
<         System.out.println("The first regex evaluation will never end in JDK <= 9:");
<         System.out.println(java.util.regex.Pattern.compile("(a+)+").matcher(
---
>         System.out.println("The first regex evaluation will end after modify:");
>         System.out.println(java.util.regex.Pattern.compile("(a+)++").matcher(
10,11c10,11
<         System.out.println("and the second regex evaluation will never end in any versions of the JDK:");
<         System.out.println(java.util.regex.Pattern.compile("(h|h|ih(((i|a|c|c|a|i|i|j|b|a|i|b|a|a|j))+h)ahbfhba|c|i)*").matcher(
---
>         System.out.println("and the second regex evaluation will end after modify:");
>         System.out.println(java.util.regex.Pattern.compile("(h|h|ih(((i|a|c|c|a|i|i|j|b|a|i|b|a|a|j))++h)ahbfhba|c|i)*").matcher(
$ javac NewBacktracking.java
$ java NewBacktracking
1.8.0_121
The first regex evaluation will end after modify:
false
and the second regex evaluation will end after modify:
true

參考資料

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末看疗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子睦授,更是在濱河造成了極大的恐慌两芳,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睹逃,死亡現(xiàn)場離奇詭異盗扇,居然都是意外死亡,警方通過查閱死者的電腦和手機沉填,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門疗隶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人翼闹,你說我怎么就攤上這事斑鼻。” “怎么了猎荠?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵坚弱,是天一觀的道長蜀备。 經常有香客問我,道長荒叶,這世上最難降的妖魔是什么碾阁? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮些楣,結果婚禮上脂凶,老公的妹妹穿的比我還像新娘。我一直安慰自己愁茁,他們只是感情好蚕钦,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鹅很,像睡著了一般嘶居。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上促煮,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天邮屁,我揣著相機與錄音,去河邊找鬼污茵。 笑死樱报,一個胖子當著我的面吹牛,可吹牛的內容都是我干的泞当。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼民珍,長吁一口氣:“原來是場噩夢啊……” “哼襟士!你這毒婦竟也來了?” 一聲冷哼從身側響起嚷量,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤陋桂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蝶溶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗜历,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年抖所,在試婚紗的時候發(fā)現(xiàn)自己被綠了梨州。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡田轧,死狀恐怖暴匠,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情傻粘,我是刑警寧澤每窖,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布帮掉,位于F島的核電站,受9級特大地震影響窒典,放射性物質發(fā)生泄漏蟆炊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一瀑志、第九天 我趴在偏房一處隱蔽的房頂上張望涩搓。 院中可真熱鬧,春花似錦后室、人聲如沸缩膝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疾层。三九已至,卻和暖如春贡避,著一層夾襖步出監(jiān)牢的瞬間痛黎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工刮吧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留湖饱,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓杀捻,卻偏偏與公主長得像井厌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子致讥,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容