防止正则表达式拒绝服务 (ReDoS)防止正则表达式拒绝服务 (ReDoS)防止正则表达式拒绝服务 (ReDoS)防止正则表达式拒绝服务 (ReDoS)
  • 文章
  • 正则表达式
    • 工具
  • 登录
找到的结果: {phrase} (显示: {results_count} 共: {results_count_total})
显示: {results_count} 共: {results_count_total}

加载更多搜索结果...

搜索范围
模糊匹配
搜索标题
搜索内容
发表 admin at 2024年3月5日
类别
  • 正则表达式
标签
防止正则表达式拒绝服务 (ReDoS)
  • 简
  • 繁
  • En
关于正则表达式 » 正则表达式范例 » 防止正则表达式拒绝服务 (ReDoS)

范例
正则表达式范例
数字范围
浮点数
电子邮件地址
IP 地址
有效日期
数字日期转换为文本
信用卡号码
比对完整行
删除重复行
编程
两个相近的字词
陷阱
灾难性回溯
过多重复
拒绝服务
将所有内容设为选用
重复捕获组
混合 Unicode 和 8 比特
本网站的其他内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式范例
正则表达式参考
替换字符串参考

防止正则表达式拒绝服务 (ReDoS)

前一个主题说明了 灾难性回溯,并提供实际范例,说明某人试图让正则表达式在自己的电脑上运作并运行良好的观点。在阅读这个主题之前,您应该先了解这些范例。

当灾难性回溯发生在您的电脑上时,会令人感到困扰。但当它发生在具有多个同时用户的服务器应用程序中时,后果可能真的会很严重。太多用户运行会出现灾难性回溯的正则表达式会导致整个服务器当机。而「太多」可能只需要服务器中 CPU 内核数量的一小部分。

如果服务器接受用户的正则表达式,那么用户可以轻松地提供一个在任何主旨上会出现灾难性回溯的正则表达式。如果服务器接受用户的主旨数据,那么用户可能会提供会触发服务器使用的正则表达式中灾难性回溯的主旨,如果这些正则表达式容易发生灾难性回溯的话。当用户可以运行上述任何一项操作时,服务器就会容易受到正则表达式拒绝服务 (ReDoS) 的影响。当足够多的用户(或一名伪装成多名用户的行为者)提供恶意的正则表达式和/或主旨进行比对时,服务器将会花费几乎所有 CPU 周期来尝试比对这些正则表达式。

处理用户提供的正则表达式

如果您的应用程序允许用户提供自己的正则表达式,那么您唯一真正的防御措施就是使用 文本导向正则表达式引擎。这些引擎不会回溯。它们的性能取决于主旨字符串的长度,而不是正则表达式的复杂度。但它们也不支持 反向引用 等依赖于回溯且许多用户预期的功能。

如果您的应用程序使用具有用户提供正则表达式的回溯引擎,那么您只能减轻灾难性回溯的后果。而且您真的需要这么做。对于正则表达式技能有限的人来说,很容易意外地制作出会退化成灾难性回溯的正则表达式。

您需要使用一个正规表达式引擎,当发生灾难性回溯时,会中止比对尝试,而不是运行到脚本崩溃或操作系统将其终止。您可以轻松地测试这一点。当正规表达式 (x\w{1,10})+y 在不断增加的 x 字符串上进行尝试时,正规表达式引擎放弃的时间应该有一个合理的限制。理想情况下,您的引擎将允许您根据您的目的设置此限制。例如,.NET 引擎允许您将逾时传递给 Regex() 构造函数。PCRE 引擎允许您设置递归限制。您的限制越低,对抗 ReDoS 的防护就越好,但中止会找到有效比对的合法正规表达式的风险就越高,即使多给一点时间。低递归限制可能会阻止长正规表达式比对。低逾时可能会过早中止搜索大型文件。

如果您的正规表达式引擎没有这些功能,您可以实作您自己的逾时。产生一个独立线程来运行正规表达式。使用逾时等待线程。如果线程在等待逾时之前完成,处理其结果。否则,终止线程并告诉用户正规表达式太过复杂。safe_regexp 套件为 Ruby 实作此功能。

检阅应用程序中的正规表达式

如果服务器只使用应用程序中硬编码的正规表达式,则您可以完全防止基于正规表达式的阻断服务攻击。您需要确保您的正规表达式不会表现出灾难性回溯,无论它们用于哪些主体。对于对正规表达式有扎实理解的人来说,这并不容易。但它确实需要小心和注意。仅测试正规表达式是否与有效主体相符是不够的。您需要通过独立于任何主体数据查看正规表达式来确保同一正规表达式的多个排列组合不可能相符同一事物。

当您让正规表达式进行选择时,就会发生排列组合。您可以使用 交替 和 量词 来运行此操作。因此,这些是您需要检查的正规表达式代码。占有 量词除外,因为它们从不回溯。

交替

选项必须互斥。如果多个选项可以相符同一文本,则如果正规表达式的剩余部分失败,引擎将尝试两者。如果选项在重复的群组中,您就会有灾难性回溯。

一个经典的范例是 (.|\s)*,用来在 regex 风格中没有「点号比对换行」模式时,比对任意数量的任何文本。如果这是较长 regex 的一部分,那么主旨字符串中足够长的一连串空白字符会中断 regex。引擎会尝试由 . 或 \s 比对的空白字符的所有可能组合。例如,3 个空白字符可以比对成 ...、..\s、.\s.、.\s\s、\s..、\s.\s、\s\s. 或 \s\s\s。那是 2^N 个排列组合。解决方法是使用 (.|\n)*,让这些选项互斥。更棒的方法是更明确地指出哪些字符真正被允许,例如 [\r\n\t\x20-\x7E]*,代表 ASCII 可打印字符、跳格键和换行字符。

两个选项部分比对相同文本是可以接受的。 [0-9]*\.[0-9]+|[0-9]+ 完全可以比对一个浮点数,其中整数部分和分数部分都是可选的。尽管只包含数字的主旨一开始会由 [0-9]* 比对,并且在 \. 失败时会造成一些回溯,但这个回溯永远不会变成灾难。即使你将它放在较长 regex 中的群组内,群组也只会运行最少的回溯。(但群组不能有量词,否则会违反嵌套量词的规则。)

量词顺序

顺序中的量化标记必须彼此互斥,或与它们之间的内容互斥。否则,两个量词都可以比对相同的文本,并且当 regex 的其余部分无法比对时,会尝试这两个量词的所有组合。群组内有交替选项的标记仍然与群组前后的任何标记顺序相同。

一个经典的范例是 a.*?b.*?c,用来比对 3 个东西,中间有「任何东西」。当 c 无法比对时,第一个 .*? 会逐字扩充到行或文件的结尾。对于每个扩充,第二个 .*? 会逐字扩充来比对行或文件的剩余部分。解决方法是体认到它们之间不能有「任何东西」。第一次运行需要在 b 停止,第二次运行需要在 c 停止。对於单一字符,a[^b]*b[^c]*c 是个简单的解决方案。否定字符类别保证重复会在分隔符号停止。如果你的正则表达式风格支持 独占量词,那么你可以使用 a[^b]*+b[^c]*+c 进一步提升性能。

对于更复杂的范例和解决方案,请参阅前一个主题中的 比对一个完整的 HTML 文件。这说明了如何在更复杂的情况下使用原子组来防止回溯。

嵌套量词

包含有量词的代码块的群组,除非群组内部被量化的代码块只能与它互斥的其他东西比对,否则群组本身不能有量词。这确保了外层量词的较少次反复与内层量词的较多次反复,不可能与外层量词的较多次反复与内层量词的较少次反复比对相同的文本。

正则表达式 (x\w{1,10})+y 比对一个或多个以 x 开头的代码,后面接着 1 到 10 个字符字符,最后接着 y。只要可以比对 y,一切都很好。当 y 遗失时,就会发生回溯。如果字符串没有太多 x,那么回溯会很快发生。只有当主旨包含一个长串行的 x 时,事情才会变成灾难。 x 和 x 不是互斥的。因此,重复的群组可以在一次反复中比对 xxxx,例如 x\w\w\w,或在两次反复中比对 x\wx\w。

若要解决此问题,您首先需要考虑是否允许 x 和 y 出现在其后的 1 至 10 个字符中。排除 x 可消除大部分回溯。剩下的部分不会造成灾难性的后果。您可以使用 字符类别减法 来排除它,例如 (x[\w-[x]]{1,10})+y,或使用 字符类别交集,例如 (x[\w&&[^x]]{1,10})+y。如果您没有这些功能,则需要拼出您要允许的字符:(x[a-wyz0-9_]{1,10})+y。

如果允许 x,则唯一的解决方案是同样不允许 y。然后,您可以让群组成为原子组或让量词成为独占量词,以消除回溯。

如果允许 x 和 y 出现在 1 至 10 个字符的串行中,则没有仅使用正则表达式的解决方案。您无法让群组成为原子组或让量词成为独占量词,因为这样 \w{1,10} 会与最后一个 y 匹配,这会导致 y 失败。

其他防御技术

除了防止如上所述的灾难性回溯之外,您应该让您的正则表达式尽可能严格。正则表达式越严格,它运行的回溯就越少,因此性能越好。即使您无法衡量性能差异,因为正则表达式不常在短字符串上使用,但正确的技术是一种习惯。它还能降低较缺乏经验的开发人员在稍后扩充您的正则表达式时引入灾难性回溯的几率。

尽可能将包含选项的群组设为原子。使用 \b(?>one|two|three)\b 来比对字词清单。

尽可能将量词设为所有格。如果重复的记号与其后内容互斥,请使用所有格量词强制运行。

使用 (否定) 字符类别,而非句点。您真正想要允许「任何内容」的情况很少见。例如,双引号字符串无法包含「任何内容」。它无法包含未转义的双引号。因此,请使用 "[^"\n]*+",而非 ".*?"。虽然两者在单独使用时找到的比对结果完全相同,但后者在贴到较长的正则表达式中时会导致灾难性回溯。前者永远不会回溯,无论正则表达式需要比对其他任何内容。

为什么要使用正则表达式?

有些人肯定会争辩说,上述内容只显示正则表达式很危险,不应该使用。然后,他们会强迫开发人员使用进程码来完成这项工作。用于比对非平凡模式的进程码很快就会变得冗长且复杂,这会增加错误的几率,并提高开发和维护代码的成本。许多模式比对问题都可以通过递归自然地解决。当无法比对大型主旨字符串时,失控的递归会导致堆栈溢出,使应用程序当机。

开发人员需要学习正确使用他们的工具。这对正则表达式来说与其他任何事物并无不同。

防止正則表達式拒絕服務 (ReDoS)
  • 简
  • 繁
  • En
關於正規表示式 » 正規表示式範例 » 防止正則表達式拒絕服務 (ReDoS)

範例
正則表達式範例
數字範圍
浮點數
電子郵件地址
IP 地址
有效日期
數字日期轉換為文字
信用卡號碼
比對完整行
刪除重複行
程式設計
兩個相近的字詞
陷阱
災難性回溯
過多重複
拒絕服務
將所有內容設為選用
重複擷取群組
混合 Unicode 和 8 位元
本網站的其他內容
簡介
正則表達式快速入門
正則表達式教學
替換字串教學
應用程式和語言
正則表達式範例
正則表達式參考
替換字串參考

防止正則表達式拒絕服務 (ReDoS)

前一個主題說明了 災難性回溯,並提供實際範例,說明某人試圖讓正則表達式在自己的電腦上運作並執行良好的觀點。在閱讀這個主題之前,您應該先了解這些範例。

當災難性回溯發生在您的電腦上時,會令人感到困擾。但當它發生在具有多個同時使用者的伺服器應用程式中時,後果可能真的會很嚴重。太多使用者執行會出現災難性回溯的正規表示式會導致整個伺服器當機。而「太多」可能只需要伺服器中 CPU 核心數量的一小部分。

如果伺服器接受使用者的正規表示式,那麼使用者可以輕鬆地提供一個在任何主旨上會出現災難性回溯的正規表示式。如果伺服器接受使用者的主旨資料,那麼使用者可能會提供會觸發伺服器使用的正規表示式中災難性回溯的主旨,如果這些正規表示式容易發生災難性回溯的話。當使用者可以執行上述任何一項操作時,伺服器就會容易受到正則表達式拒絕服務 (ReDoS) 的影響。當足夠多的使用者(或一名偽裝成多名使用者的行為者)提供惡意的正規表示式和/或主旨進行比對時,伺服器將會花費幾乎所有 CPU 週期來嘗試比對這些正規表示式。

處理使用者提供的正規表示式

如果您的應用程式允許使用者提供自己的正規表示式,那麼您唯一真正的防禦措施就是使用 文字導向正規表示式引擎。這些引擎不會回溯。它們的效能取決於主旨字串的長度,而不是正則表達式的複雜度。但它們也不支援 反向參照 等依賴於回溯且許多使用者預期的功能。

如果您的應用程式使用具有使用者提供正規表示式的回溯引擎,那麼您只能減輕災難性回溯的後果。而且您真的需要這麼做。對於正規表示式技能有限的人來說,很容易意外地製作出會退化成災難性回溯的正規表示式。

您需要使用一個正規表達式引擎,當發生災難性回溯時,會中止比對嘗試,而不是執行到腳本崩潰或作業系統將其終止。您可以輕鬆地測試這一點。當正規表達式 (x\w{1,10})+y 在不斷增加的 x 字串上進行嘗試時,正規表達式引擎放棄的時間應該有一個合理的限制。理想情況下,您的引擎將允許您根據您的目的設定此限制。例如,.NET 引擎允許您將逾時傳遞給 Regex() 建構函式。PCRE 引擎允許您設定遞迴限制。您的限制越低,對抗 ReDoS 的防護就越好,但中止會找到有效比對的合法正規表達式的風險就越高,即使多給一點時間。低遞迴限制可能會阻止長正規表達式比對。低逾時可能會過早中止搜尋大型檔案。

如果您的正規表達式引擎沒有這些功能,您可以實作您自己的逾時。產生一個獨立執行緒來執行正規表達式。使用逾時等待執行緒。如果執行緒在等待逾時之前完成,處理其結果。否則,終止執行緒並告訴使用者正規表達式太過複雜。safe_regexp 套件為 Ruby 實作此功能。

檢閱應用程式中的正規表達式

如果伺服器只使用應用程式中硬編碼的正規表達式,則您可以完全防止基於正規表達式的阻斷服務攻擊。您需要確保您的正規表達式不會表現出災難性回溯,無論它們用於哪些主體。對於對正規表達式有紮實理解的人來說,這並不容易。但它確實需要小心和注意。僅測試正規表達式是否與有效主體相符是不夠的。您需要透過獨立於任何主體資料查看正規表達式來確保同一正規表達式的多個排列組合不可能相符同一事物。

當您讓正規表達式進行選擇時,就會發生排列組合。您可以使用 交替 和 量詞 來執行此操作。因此,這些是您需要檢查的正規表達式代碼。佔有 量詞除外,因為它們從不回溯。

交替

選項必須互斥。如果多個選項可以相符同一文字,則如果正規表達式的剩餘部分失敗,引擎將嘗試兩者。如果選項在重複的群組中,您就會有災難性回溯。

一個經典的範例是 (.|\s)*,用來在 regex 風格中沒有「點號比對換行」模式時,比對任意數量的任何文字。如果這是較長 regex 的一部分,那麼主旨字串中足夠長的一連串空白字元會中斷 regex。引擎會嘗試由 . 或 \s 比對的空白字元的所有可能組合。例如,3 個空白字元可以比對成 ...、..\s、.\s.、.\s\s、\s..、\s.\s、\s\s. 或 \s\s\s。那是 2^N 個排列組合。解決方法是使用 (.|\n)*,讓這些選項互斥。更棒的方法是更明確地指出哪些字元真正被允許,例如 [\r\n\t\x20-\x7E]*,代表 ASCII 可列印字元、跳格鍵和換行字元。

兩個選項部分比對相同文字是可以接受的。 [0-9]*\.[0-9]+|[0-9]+ 完全可以比對一個浮點數,其中整數部分和分數部分都是可選的。儘管只包含數字的主旨一開始會由 [0-9]* 比對,並且在 \. 失敗時會造成一些回溯,但這個回溯永遠不會變成災難。即使你將它放在較長 regex 中的群組內,群組也只會執行最少的回溯。(但群組不能有量詞,否則會違反巢狀量詞的規則。)

量詞順序

順序中的量化標記必須彼此互斥,或與它們之間的內容互斥。否則,兩個量詞都可以比對相同的文字,並且當 regex 的其餘部分無法比對時,會嘗試這兩個量詞的所有組合。群組內有交替選項的標記仍然與群組前後的任何標記順序相同。

一個經典的範例是 a.*?b.*?c,用來比對 3 個東西,中間有「任何東西」。當 c 無法比對時,第一個 .*? 會逐字擴充到行或檔案的結尾。對於每個擴充,第二個 .*? 會逐字擴充來比對行或檔案的剩餘部分。解決方法是體認到它們之間不能有「任何東西」。第一次執行需要在 b 停止,第二次執行需要在 c 停止。對於單一字元,a[^b]*b[^c]*c 是個簡單的解決方案。否定字元類別保證重複會在分隔符號停止。如果你的正規表示法風格支援 獨佔量詞,那麼你可以使用 a[^b]*+b[^c]*+c 進一步提升效能。

對於更複雜的範例和解決方案,請參閱前一個主題中的 比對一個完整的 HTML 檔案。這說明了如何在更複雜的情況下使用原子群組來防止回溯。

巢狀量詞

包含有量詞的代碼塊的群組,除非群組內部被量化的代碼塊只能與它互斥的其他東西比對,否則群組本身不能有量詞。這確保了外層量詞的較少次反覆與內層量詞的較多次反覆,不可能與外層量詞的較多次反覆與內層量詞的較少次反覆比對相同的文字。

正規表示法 (x\w{1,10})+y 比對一個或多個以 x 開頭的代碼,後面接著 1 到 10 個字元字元,最後接著 y。只要可以比對 y,一切都很好。當 y 遺失時,就會發生回溯。如果字串沒有太多 x,那麼回溯會很快發生。只有當主旨包含一個長序列的 x 時,事情才會變成災難。 x 和 x 不是互斥的。因此,重複的群組可以在一次反覆中比對 xxxx,例如 x\w\w\w,或在兩次反覆中比對 x\wx\w。

若要解決此問題,您首先需要考慮是否允許 x 和 y 出現在其後的 1 至 10 個字元中。排除 x 可消除大部分回溯。剩下的部分不會造成災難性的後果。您可以使用 字元類別減法 來排除它,例如 (x[\w-[x]]{1,10})+y,或使用 字元類別交集,例如 (x[\w&&[^x]]{1,10})+y。如果您沒有這些功能,則需要拼出您要允許的字元:(x[a-wyz0-9_]{1,10})+y。

如果允許 x,則唯一的解決方案是同樣不允許 y。然後,您可以讓群組成為原子群組或讓量詞成為獨佔量詞,以消除回溯。

如果允許 x 和 y 出現在 1 至 10 個字元的序列中,則沒有僅使用正規表示式的解決方案。您無法讓群組成為原子群組或讓量詞成為獨佔量詞,因為這樣 \w{1,10} 會與最後一個 y 匹配,這會導致 y 失敗。

其他防禦技術

除了防止如上所述的災難性回溯之外,您應該讓您的正規表示式盡可能嚴格。正規表示式越嚴格,它執行的回溯就越少,因此效能越好。即使您無法衡量效能差異,因為正規表示式不常在短字串上使用,但正確的技術是一種習慣。它還能降低較缺乏經驗的開發人員在稍後擴充您的正規表示式時引入災難性回溯的機率。

盡可能將包含選項的群組設為原子。使用 \b(?>one|two|three)\b 來比對字詞清單。

盡可能將量詞設為所有格。如果重複的記號與其後內容互斥,請使用所有格量詞強制執行。

使用 (否定) 字元類別,而非句點。您真正想要允許「任何內容」的情況很少見。例如,雙引號字串無法包含「任何內容」。它無法包含未跳脫的雙引號。因此,請使用 "[^"\n]*+",而非 ".*?"。雖然兩者在單獨使用時找到的比對結果完全相同,但後者在貼到較長的正規表示式中時會導致災難性回溯。前者永遠不會回溯,無論正規表示式需要比對其他任何內容。

為什麼要使用正規表示式?

有些人肯定會爭辯說,上述內容只顯示正規表示式很危險,不應該使用。然後,他們會強迫開發人員使用程序碼來完成這項工作。用於比對非平凡模式的程序碼很快就會變得冗長且複雜,這會增加錯誤的機率,並提高開發和維護程式碼的成本。許多模式比對問題都可以透過遞迴自然地解決。當無法比對大型主旨字串時,失控的遞迴會導致堆疊溢位,使應用程式當機。

開發人員需要學習正確使用他們的工具。這對正規表示式來說與其他任何事物並無不同。

Preventing Regular Expression Denial of Service (ReDoS)
  • 简
  • 繁
  • En
About Regular Expressions » Sample Regular Expressions » Preventing Regular Expression Denial of Service (ReDoS)

Examples
Regular Expressions Examples
Numeric Ranges
Floating Point Numbers
Email Addresses
IP Addresses
Valid Dates
Numeric Dates to Text
Credit Card Numbers
Matching Complete Lines
Deleting Duplicate Lines
Programming
Two Near Words
Pitfalls
Catastrophic Backtracking
Too Many Repetitions
Denial of Service
Making Everything Optional
Repeated Capturing Group
Mixing Unicode & 8-bit
More on This Site
Introduction
Regular Expressions Quick Start
Regular Expressions Tutorial
Replacement Strings Tutorial
Applications and Languages
Regular Expressions Examples
Regular Expressions Reference
Replacement Strings Reference

Preventing Regular Expression Denial of Service (ReDoS)

The previous topic explains catastrophic backtracking with practical examples from the perspective of somebody trying to get their regular expressions to work and perform well on their own PC. You should understand those examples before reading this topic.

It’s annoying when catastrophic backtracking happens on your PC. But when it happens in a server application with multiple concurrent users, it can really be catastrophic. Too many users running regexes that exhibit catastrophic backtracking will bring down the whole server. And “too many” need only be as few as the number of CPU cores in the server.

If the server accepts regexes from the user, then the user can easily provide one that exhibits catastrophic backtracking on any subject. If the server accepts subject data from the user, then the user may be able to provide subjects that trigger catastrophic backtracking in regexes used by the server, if those regexes are predisposed to catastrophic backtracking. When the user can do either of those things, the server is susceptible to regular expression denial of service (ReDoS). When enough users (or one actor masquerading as many users) provide malicious regexes and/or subjects to match against, the server will be spending nearly all its CPU cycles on trying to match those regexes.

Handling Regexes Provided by The User

If your application allows the user to provide their own regexes, then your only real defense is to use a text-directed regex engine. Those engines don’t backtrack. Their performance depends on the length of the subject string, not the complexity of the regular expression. But they also don’t support features like backreferences that depend on backtracking and that many users expect.

If your application uses a backtracking engine with user-provided regexes, then you can only mitigate the consequences of catastrophic backtracking. And you’ll really need to do so. It’s very easy for people with limited regex skills to accidentally craft one that degenerates into catastrophic backtracking.

You’ll need to use a regex engine that aborts the match attempt when catastrophic backtracking occurs rather than running until the script crashes or the OS kills it. You can easily test this. When the regex (x\w{1,10})+y is attempted on an ever growing string of x’s there should be a reasonable limit on how long it takes for the regex engine to give up. Ideally your engine will allow you to configure this limit for your purposes. The .NET engine, for example, allows you to pass a timeout to the Regex() constructor. The PCRE engine allows you to set recursion limits. The lower your limits the better the protection against ReDoS, but higher the risk of aborting legitimate regexes that would find a valid match given slightly more time. Low recursion limits may prevent long regex matches. Low timeouts may abort searches through large files too early.

If your regex engine has no such features, you could implement your own timeout. Spawn a separate thread to execute the regular expression. Wait on the thread with a timeout. If the thread finishes before the wait times out, process its result. Otherwise, kill the thread and tell the user the regex is too complex. The safe_regexp package implements this for Ruby.

Reviewing Regexes in The Application

If the server only uses regexes that are hard-coded in your application, then you can prevent regex-based denial of service attacks entirely. You need to make sure that your regexes won’t exhibit catastrophic backtracking regardless of the subjects they’re used on. This isn’t particularly difficult for somebody with a solid grasp of regular expressions. But it does require care and attention. It’s not enough to just test that the regex matches valid subjects. You need to make sure, by looking at the regex independently of any subject data, that it is not possible for multiple permutations of the same regex to match the same thing.

Permutations occur when you give the regular expression a choice. You can do this with alternation and with quantifiers. So these are the regex tokens you need to inspect. Possessive quantifiers are excepted, because they never backtrack.

Alternation

Alternatives must be mutually exclusive. If multiple alternatives can match the same text then the engine will try both if the remainder of the regex fails. If the alternatives are in a group that is repeated, you have catastrophic backtracking.

A classic example is (.|\s)* to match any amount of any text when the regex flavor does not have a “dot matches line breaks” mode. If this is part of a longer regex then a subject string with a sufficiently long run of spaces will break the regex. The engine will try every possible combination of the spaces being matched by . or \s. For example, 3 spaces could be matched as ..., ..\s, .\s., .\s\s, \s.., \s.\s, \s\s., or \s\s\s. That’s 2^N permutations. The fix is to use (.|\n)* to make the alternatives mutually exclusive. Even better to be more specific about which characters are really allowed, such as [\r\n\t\x20-\x7E]* for ASCII printables, tabs, and line breaks.

It is acceptable for two alternatives to partially match the same text. [0-9]*\.[0-9]+|[0-9]+ is perfectly fine to match a floating point number with optional integer part and optional fraction. Though a subject that consists of only digits is initially matched by [0-9]* and does cause some backtracking when \. fails, this backtracking never becomes catastrophic. Even if you put this inside a group in a longer regex, the group only does a minimal amount of backtracking. (But the group mustn’t have a quantifier or it will fall foul of the rule for nested quantifiers.)

Quantifiers in Sequence

Quantified tokens that are in sequence must either be mutually exclusive with each other or be mutually exclusive with what comes between them. Otherwise both can match the same text and all combinations of the two quantifiers will be tried when the remainder of the regex fails to match. A token inside a group with alternation is still in sequence with any token before or after the group.

A classic example is a.*?b.*?c to match 3 things with “anything” between them. When c can’t be matched the first .*? expands character by character until the end of the line or file. For each expansion the second .*? expands character by character to match the remainder of the line or file. The fix is to realize that you can’t have “anything” between them. The first run needs to stop at b and the second run needs to stop at c. With single characters a[^b]*b[^c]*c is an easy solution. The negated character classes guarantee the repetition stops at the delimiter. If your regex flavor supports possessive quantifiers then you can use a[^b]*+b[^c]*+c to further increase performance.

For a more complex example and solution, see matching a complete HTML file in the previous topic. This explains how you can use atomic grouping to prevent backtracking in more complex situations.

Nested Quantifiers

A group that contains a token with a quantifier must not have a quantifier of its own unless the quantified token inside the group can only be matched with something else that is mutually exclusive with it. That ensures that there is no way that fewer iterations of the outer quantifier with more iterations of the inner quantifier can match the same text as more iterations of the outer quantifier with fewer iterations of the inner quantifier.

The regex (x\w{1,10})+y matches a sequence of one or more codes that start with an x followed by 1 to 10 word characters, all followed by a y. All is well as long as the y can be matched. When the y is missing, backtracking occurs. If the string doesn’t have too many x’s then backtracking happens very quickly. Things only turn catastrophic when the subject contains a long sequence of x’s. x and x are not mutually exclusive. So the repeated group can match xxxx in one iteration as x\w\w\w or in two iterations as x\wx\w.

To solve this, you first need to consider whether x and y should be allowed in the 1 to 10 characters that follow it. Excluding the x eliminates most backtracking. What’s left won’t be catastrophic. You could exclude it with character class subtraction as in (x[\w-[x]]{1,10})+y or with character class intersection as in (x[\w&&[^x]]{1,10})+y. If you don’t have those features you’ll need to spell out the characters you want to allow: (x[a-wyz0-9_]{1,10})+y.

If the x should be allowed then your only solution is to disallow the y in the same way. Then you can make the group atomic or the quantifier possessive to eliminate the backtracking.

If both x and y should be allowed in the sequences of 1 to 10 characters, then there is no regex-only solution. You can’t make the group atomic or the quantifier possessive as then \w{1,10} matches the final y which causes y to fail.

Other Defensive Techniques

In addition to preventing catastrophic backtracking as explained above, you should make your regular expressions as strict as possible. The stricter the regex, the less backtracking it does and thus the better it performs. Even if you can’t measure the performance difference because the regex is used infrequently on short strings, proper technique is a habit. It also reduces the chance that a less experienced developer introduces catastrophic backtracking when they extend your regex later.

Make groups that contain alternatives atomic as much as you can. Use \b(?>one|two|three)\b to match a list of words.

Make quantifiers possessive as much as you can. If a repeated token is mutually exclusive with what follows, enforce that with a possessive quantifier.

Use (negated) character classes instead of the dot. It’s rare that you really want to allow “anything”. A double-quoted string, for example, can’t contain “anything”. It can’t contain unescaped double quotes. So use "[^"\n]*+" instead of ".*?". Though both find exactly the same matches when used on their own, the latter can lead to catastrophic backtracking when pasted into a longer regex. The former never backtracks regardless of anything else the regex needs to match.

Why Use Regexes at All?

Some would certainly argue that the above only shows that regexes are dangerous and that they should not be used. They’ll then force developers to do the job with procedural code. Procedural code to match non-trivial patterns quickly becomes long and complicated, increasing the chance of bugs and the cost to develop and maintain the code. Many pattern matching problems are naturally solved with recursion. And when a large subject string can’t be matched, runaway recursion leads to stack overflows that crash the application.

Developers need to learn to correctly use their tools. This is no different for regular expressions than for anything else.

©2015-2025 艾丽卡 support@alaica.com