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