史敏才
Python開發(fā)者們在使用標準庫和通用框架時,都以為自己的程序具有可靠的安全性。然而,Python就像在任何其他編程語言一樣,有一些特性可能會被開發(fā)者們誤解或誤用。如果你正在使用這些特性,請一定要排查Python代碼。
Python支持以優(yōu)化的方式執(zhí)行代碼。這使代碼運行得更快,內(nèi)存用得更少,當程序被大規(guī)模使用,或者可用的資源很少時,這種方法尤其有效。一些預打包的Python程序提供了優(yōu)化的字節(jié)碼。
然而,當代碼被優(yōu)化時,所有的assert語句都會被忽略。開發(fā)者有時會使用它們來判斷代碼中的某些條件。例如,如果使用斷言來作身份驗證檢查,則可能導致安全繞過。
os.makdirs函數(shù)可以在操作系統(tǒng)中創(chuàng)建一個或多個文件夾,它的第二個參數(shù)mode用于指定創(chuàng)建的文件夾的默認權限。
在低于Python3.6版本中,創(chuàng)建出的文件夾A、B和C的權限都是700。但是,在Python 3.6以上的版本中,只有最后一個文件夾C的權限為700,其他文件夾A和B的權限為默認的755。因此,在Python 3.6以上的版本中,os.makdirs函數(shù)等價于Linux的這條命令:mkdir -m 700 -p A/B/C。有些開發(fā)者沒有意識到版本之間的差異,這已經(jīng)在Django中造成了一個權限越級漏洞(cve - 2022 -24583),無獨有偶,這在WordPress中也造成了一個加固繞過問題。
os.path.join(path, *paths)函數(shù)用于將多個文件路徑連接成一個組合的路徑。第一個參數(shù)通常包含了基礎路徑,而之后的每個參數(shù)都被當做組件拼接到基礎路徑后。
然而,這個函數(shù)有一個少有人知的特性,如果拼接的某個路徑以/開頭,那么包括基礎路徑在內(nèi)的所有前綴路徑都將被刪除,該路徑將被視為絕對路徑。
tempfile.NamedTemporaryFile函數(shù)用于創(chuàng)建具有特定名稱的臨時文件。但是,prefix(前綴)和suffix(后綴)參數(shù)很容易受到路徑遍歷攻擊(Issue 35278)。如果攻擊者控制了這些參數(shù)之一,他可以在文件系統(tǒng)中的任意位置創(chuàng)建出一個臨時文件。看起來,這可能是無害的,但它會為攻擊者創(chuàng)造出挖掘復雜漏洞的基礎。
在Web應用中,通常需要解壓上傳后的壓縮文件。在Python中,很多人都知道TarFile.extractall與TarFile.extract函數(shù)容易受到Zip Slip攻擊。攻擊者通過篡改壓縮包中的文件名,使其包含路徑遍歷(../)字符,從而發(fā)起攻擊。
這就是為什么壓縮文件應該始終被視為不受信來源的原因,zipfile.extractall與zipfile.extract函數(shù)可以對zip內(nèi)容進行清洗,從而防止這類路徑遍歷漏洞。但是,這并不意味著在ZipFile庫中不會出現(xiàn)路徑遍歷漏洞。
壓縮包中的文件應該被看作是不受信任的,如果不使用zipfile.extractall或者zipfile.extract,就必須對zip內(nèi)文件的名稱進行“消毒”,例如使用os.path.basename。否則,它可能導致嚴重的安全漏洞,就像在NLTK Downloader(CVE-2019-14751)中發(fā)現(xiàn)的那樣。
正則表達式(regex)是大多數(shù)Web程序不可或缺的一部分。我們經(jīng)常能看到它被自定義的Web應用防火墻(WAF)用來作輸入驗證,例如檢測惡意字符串,在Python中,re.match和re.search之間有著細微區(qū)別的。總之,不建議使用正則表達式黑名單進行任何安全檢查。
Unicode支持用多種形式來表示字符,并將這些字符映射到碼點。在Unicode標準中,不同的Unicode字符有4種歸一化方案。程序可以使用這些歸一化方法,以獨立于人類語言的標準方式來存儲數(shù)據(jù),例如用戶名。
然而,攻擊者可以利用這些歸一化,這已經(jīng)導致了Python的urllib出現(xiàn)漏洞(CVE-2019-9636)。
前文說過,Unicode字符會被映射成碼點,然而,有許多不同的語言,Unicode試圖將它們統(tǒng)一起來。這就意味著不同的字符很有可能擁有相同的layout。例如,小寫的土耳其語(沒有點)的字符是英語中大寫的I。在拉丁字母中,字符i也是用大寫的I表示。在Unicode標準中,這2個不同的字符都以大寫形式映射到同一個碼點。這種行為是可以被利用的,實際上已經(jīng)在Django中導致了一個嚴重的漏洞(CVE-2019-19844)。

假設數(shù)據(jù)庫中存在一個郵箱地址為foo@mix.com的用戶。那么,攻擊者可以簡單地傳入foo@m?x.com作為email,其中i被替換為土耳其語。代碼將郵箱轉換成大寫,結果是FOO@MIX.COM。這意味著找到了一個用戶,因此會發(fā)送一封重置密碼的郵件。
然而,郵件被發(fā)送到未轉換的郵件地址,也就是包含了土耳其語的,換句話說,其他用戶的密碼被發(fā)送到了攻擊者控制的郵件地址。
在Python 3.8以下的版本3.8中,IP地址會被ipaddress庫歸一化,因此前綴的零會被刪除。這種行為乍一看可能是無害的,但它已經(jīng)在Django中導致了一個高嚴重性的漏洞(CVE-2021-33571)。攻擊者可以利用歸一化繞過校驗程序,發(fā)起服務端請求偽造攻擊(SSRF)。
在高于Python3.7版本中,urllib.parse.parse_qsl函數(shù)允許使用“;”和“&”字符作為URL的查詢變量的分隔符,有趣的是“;”字符不能被其他語言識別為分隔符,這種查詢參數(shù)解析的差異可能會導致致命的安全漏洞,比如Django中的Web緩存投毒漏洞(CVE-2021-23336)。
總之,安全陷阱可能出現(xiàn)在各種操作中,從處理文件、目錄、壓縮文件、URL、IP到簡單的字符串,一種常見的情況是庫函數(shù)的使用,這些函數(shù)可能有意想不到的行為。這提醒我們一定要升級到最新版本,并仔細閱讀文檔。