03 / 指南
安全測試正規表示式的工作流程
面向 JavaScript 正規表示式的編寫—測試—重構循環:真實樣本資料、災難性回溯防護、錨定紀律、旗標語意,以及為什麼在瀏覽器中測試能讓模式保持私密。
循環:編寫、測試、重構
可靠的正規表示式從來不是一次寫成的。從能匹配一個真實例子的最小模式開始,執行它,再對照下一個例子放寬或收緊。樣本文字是規格,模式是實作。
回饋即時,循環才快:每次按鍵都重新求值的測試器把每次修改變成一個有即時答案的實驗,這就是為什麼專用工作區勝過手工反覆跑腳本。
- 從樣本開始對著具體樣本寫模式,而不是憑記憶:貼上一行你打算解析的日誌或文件的真實內容。
- 測試每次修改每次修改後都測試——正規表示式沒有編譯警告,唯一的回饋就是它匹配了什麼、漏掉了什麼。
- 為讀者重構通過之後為閱讀而重構:命名擷取群組與非擷取群組讓能運作的模式對下一位讀者也能存活。
敬畏災難性回溯
JavaScript 的正規表示式引擎會回溯:匹配失敗時,它會用不同的量詞長度重試。(a+)+ 這類巢狀量詞作用在險些匹配的字串上,會讓重試次數指數級爆炸——在良好輸入上瞬間作答的模式,在惡意輸入上可能跑好幾年。攻擊者稱之為 ReDoS。
JavaScript 正規表示式測試器約束了爆炸半徑:模式在專用 Web Worker 中以 650 毫秒預算執行,失控的表達式會被回報為明確的逾時,而不是凍結分頁。如果你的模式在真實輸入上撞上這條上限,把它當作一個發現——同樣的輸入在生產環境會卡住事件循環。
- 巢狀量詞經典形態是量詞群組內含量詞——(a+)+、(\w+)*——同一字串的許多種劃分都能滿足內部模式。
- 歧義交替歧義交替也會引爆:(a|aa)+ 給引擎兩種匹配每個額外字元的方式,搜尋樹隨字元數翻倍。
- 結構性修復修復是結構性的:讓內部匹配無歧義、用顯式長度限制約束,並且務必測試險些匹配的輸入,而不只是合法輸入。
先錨定,再信任匹配
能找到子字串的模式,不等於能校驗值的模式。/\d+/ 會在 abc123xyz 內部匹配;/^\d+$/ 只接受純數字。大多數校驗缺陷是缺少錨點,而大多數提取缺陷是不該有的錨點。
- 開頭與結尾^ 與 $ 把匹配釘在字串的開頭與結尾——開啟多行旗標時則釘在每一行,這會悄悄改變「整個輸入」的含義。
- 詞邊界\b 詞邊界防止短模式匹配進更長的單詞:\bcat\b 找的是那隻動物,而不是 concatenate 裡的 cat。
- 搜尋還是校驗一開始就決定搜尋還是校驗:提取模式通常不要錨點,校驗模式幾乎總是兩個都要。
用真實樣本資料測試
用三個整潔例子證明過的模式,會在生產環境的第四個真實輸入上失敗。構建反映輸入分布的樣本集:最長的合法值、空字串、空白變體、Unicode 名字,以及那個幾乎該匹配但必須不匹配的險些樣本。
- 險些樣本包含險些樣本:郵箱模式應該用 a@b 和 a b@c.d 測試,而不只是格式良好的地址。
- Unicode資料裡有 Unicode 就測 Unicode:\w 和 . 在 u 旗標下含義改變,帶重音的名字會戳破 ASCII 時代的假設。
- 重跑整個集合把樣本集放在模式旁邊;模式一變,重跑整個集合,而不只是促成修改的那個用例。
旗標改變的是語言,不只是搜尋
旗標是模式含義的一部分。g 決定你拿到第一個匹配還是全部,i 摺疊大小寫,m 把 ^ 和 $ 重新錨定到行,s 讓點號跨行,u 把引擎切換為語法更嚴格的 Unicode 感知模式。
工作區按你選擇的旗標求值,列出每個匹配的起止偏移、帶各自位置的編號與命名擷取群組,以及即時的替換預覽——旗標的變化體現為結果上的具體差異,而不是理論。
- g 與 lastIndex沒有 g,test() 與 match() 只看到第一個命中;有了 g,正規表示式物件攜帶 lastIndex 狀態,反覆呼叫 test() 會交替回傳結果——一個臭名遠播的生產缺陷。
- s 放寬點號s(dotAll)讓 . 匹配換行,全域啟用時可能悄悄放大舊模式的胃口。
- u 及其夥伴u 讓字元類別與轉義符合 Unicode 並拒絕歧義的舊式語法;測試器還支援 d(匹配索引)、y(黏性)與 v(Unicode 集合)。
讓模式與樣本留在本機
測試資料往往就是真實資料:客戶郵箱、內部日誌行、事故報告裡的識別碼。把它貼進託管測試器就是把它送進別人的基礎設施。基於瀏覽器的測試器把整個工作階段留在分頁裡——模式與樣本文字由你自己裝置的引擎求值,絕不傳輸。
本機執行時對上限很明確:模式最長 4,096 字元,樣本文字最長 100,000 字元,替換串最長 20,000,單次執行最多回報 1,000 個匹配。這些限制面向開發與除錯;批次日誌分析屬於本機腳本,而不是瀏覽器分頁。
只複製出你需要的東西——完成的模式與替換串——把樣本資料留在它開始的地方:你的機器上。
在本機測試一個模式提取出的資料通常落進 JSON。
模式從原始文字中抽出值之後,用一套確定性的本機流程檢視並 diff 得到的負載。