6. 不完整的正則表達式匹配
正則表達式(regex)是大多數(shù) Web 程序不可或缺的一部分。我們經(jīng)常能看到它被自定義的 Web 應(yīng)用防火墻(WAF,Web Application Firewalls)用來作輸入驗證,例如檢測惡意字符串。在 Python 中,re.match 和 re.search 之間有著細微的區(qū)別,我們將在下面的代碼片段中演示。
def is_sql_injection(request):
pattern = re.compile(r".*(union)|(select).*")
name_to_test = request.GET['name']
if re.search(pattern, name_to_test):
return True
return False
在第 2 行中,我們定義了一個匹配 union 或者 select 的模式,以檢測可能的 SQL 注入。這是一個糟糕的寫法,因為你可以輕易地繞過這些黑名單,但我們已經(jīng)在線上的程序中見過它。在第 4 行中,函數(shù) re.match 使用前面定義好的模式,檢查第 3 行中的用戶輸入內(nèi)容是否包含這些惡意的值。
然而,與 re.search 函數(shù)不同的是,re.match 函數(shù)不匹配新行。例如,如果攻擊者提交了值 aaaaaa \n union select,這個輸入就匹配不上正則表達式。因此,檢查可以被繞過,失去保護作用。
總而言之,我們不建議使用正則表達式黑名單進行任何安全檢查。
7. Unicode 清洗器繞過
Unicode 支持用多種形式來表示字符,并將這些字符映射到碼點。在 Unicode 標準中,不同的 Unicode 字符有四種歸一化方案。程序可以使用這些歸一化方法,以獨立于人類語言的標準方式來存儲數(shù)據(jù),例如用戶名。
然而,攻擊者可以利用這些歸一化,這已經(jīng)導(dǎo)致了 Python 的 urllib 出現(xiàn)漏洞(CVE-2019-9636)。下面的代碼片段演示了一個基于 NFKC 歸一化的跨站點腳本漏洞(XSS,Cross-Site Scripting)。
import unicodedata
from django.shortcuts import render
from django.utils.html import escape
def render_input(request):
user_input = escape(request.GET['p'])
normalized_user_input = unicodedata.normalize("NFKC", user_input)
context = {'my_input': normalized_user_input}
return render(request, 'test.html', context)
在第 6 行中,用戶輸入的內(nèi)容被 Django 的 escape 函數(shù)處理了,以防止 XSS 漏洞。在第 7 行中,經(jīng)過清洗的輸入被 NFKC 算法歸一化,以便在第 8-9 行中通過 test.html 模板正確地渲染。
templates/test.html
{{ my_input | safe}}
在模板 test.html 中,第 4 行的變量 my_input 被標記為安全的,因為開發(fā)人員預(yù)期有特殊字符,并且認為該變量已經(jīng)被 escape 函數(shù)清洗了。通過標記關(guān)鍵字 safe, Django 不會再次對變量進行清洗。
但是,由于第 7 行(view.py)的歸一化,字符“%EF%B9%A4”會被轉(zhuǎn)換為“<”,“%EF%B9%A5”被轉(zhuǎn)換為“>”。這導(dǎo)致攻擊者可以注入任意的 HTML 標記,進而觸發(fā) XSS 漏洞。為了防止這個漏洞,就應(yīng)該在把用戶輸入做完歸一化之后,再進行清洗。
8. Unicode 編碼碰撞
前文說過,Unicode 字符會被映射成碼點。然而,有許多不同的人類語言,Unicode 試圖將它們統(tǒng)一起來。這就意味著不同的字符很有可能擁有相同的“l(fā)ayout”。例如,小寫的土耳其語 ?(沒有點)的字符是英語中大寫的 I。在拉丁字母中,字符 i 也是用大寫的 I 表示。在 Unicode 標準中,這兩個不同的字符都以大寫形式映射到同一個碼點。
這種行為是可以被利用的,實際上已經(jīng)在 Django 中導(dǎo)致了一個嚴重的漏洞(CVE-2019-19844)。下面的代碼是一個重置密碼的示例。
from django.core.mail import send_mail
from django.http import HttpResponse
from vuln.models import User
def reset_pw(request):
email = request.GET['email']
result = User.objects.filter(email__exact=email.upper()).first()
if not result:
return HttpResponse("User not found!")
send_mail('Reset Password','Your new pw: 123456.', 'from@example.com', [email], fail_silently=False)
return HttpResponse("Password reset email send!")
第 6 行代碼獲取了用戶輸入的 email,第 7-9 行代碼檢查這個 email 值,查找是否存在具有該 email 的用戶。如果用戶存在,則第 10 行代碼依據(jù)第 6 行中輸入的 email 地址,給用戶發(fā)送郵件。需要指出的是,第 7-9 行中對郵件地址的檢查是不區(qū)分大小寫的,使用了 upper 函數(shù)。
至于攻擊,我們假設(shè)數(shù)據(jù)庫中存在一個郵箱地址為 foo@mix.com 的用戶。那么,攻擊者可以簡單地傳入 foo@m?x.com 作為第 6 行中的 email,其中 i 被替換為土耳其語 ?。第 7 行代碼將郵箱轉(zhuǎn)換成大寫,結(jié)果是 FOO@MIX.COM。這意味著找到了一個用戶,因此會發(fā)送一封重置密碼的郵件。
然而,郵件被發(fā)送到第 6 行未轉(zhuǎn)換的郵件地址,也就是包含了土耳其語的 ?。換句話說,其他用戶的密碼被發(fā)送到了攻擊者控制的郵件地址。為了防止這個漏洞,可以將第 10 行替換成使用數(shù)據(jù)庫中的用戶郵箱。即使發(fā)生編碼沖突,攻擊者在這種情況下也得不到任何好處。
9. IP 地址歸一化
在 Python < 3.8 中,IP 地址會被 ipaddress 庫歸一化,因此前綴的零會被刪除。這種行為乍一看可能是無害的,但它已經(jīng)在 Django 中導(dǎo)致了一個高嚴重性的漏洞(CVE-2021-33571)。攻擊者可以利用歸一化繞過校驗程序,發(fā)起服務(wù)端請求偽造攻擊(SSRF,Server-Side Request Forgery)。
下面的代碼展示了如何繞過這樣的校驗器。
import requests
import ipaddress
def send_request(request):
ip = request.GET['ip']
try:
if ip in ["127.0.0.1", "0.0.0.0"]:
return HttpResponse("Not allowed!")
ip = str(ipaddress.IPv4Address(ip))
except ipaddress.AddressValueError:
return HttpResponse("Error at validation!")
requests.get('https://' + ip)
return HttpResponse("Request send!")
第 5 行代碼獲取用戶傳入的一個 IP 地址,第 7 行代碼使用一個黑名單來檢查該 IP 是否為本地地址,以防止可能的 SSRF 漏洞。這份黑名單并不完整,僅作為示例。
第 9 行代碼檢查該 IP 是否為 IPv4 地址,同時將 IP 歸一化。在完成驗證后,第 12 行代碼會對該 IP 發(fā)起實際的請求。
但是,攻擊者可以傳入 127.0.001 這樣的 IP 地址,在第 7 行的黑名單列表中找不到。然后,第 9 行代碼使用 ipaddress.IPv4Address 將 IP 歸一化為 127.0.0.1。因此,攻擊者就能夠繞過 SSRF 校驗器,并向本地網(wǎng)絡(luò)地址發(fā)送請求。
10. URL 查詢參數(shù)解析
在 Python < 3.7 中,urllib.parse.parse_qsl 函數(shù)允許使用“;”和“&”字符作為 URL 的查詢變量的分隔符。有趣的是“;”字符不能被其它語言識別為分隔符。
在下面的例子中,我們將展示為什么這種行為會導(dǎo)致漏洞。假設(shè)我們正在運行一個基礎(chǔ)設(shè)施,其中前端是一個 PHP 程序,后端則是一個 Python 程序。
攻擊者向 PHP 前端發(fā)送以下的 GET 請求:
GET https://victim.com/?a=1;b=2
PHP 前端只識別出一個查詢參數(shù)“a”,其內(nèi)容為“1;b=2”。PHP 不把“;”字符作為查詢參數(shù)的分隔符?,F(xiàn)在,前端會將攻擊者的請求直接轉(zhuǎn)發(fā)給內(nèi)部的 Python 程序:
GET https://internal.backend/?a=1;b=2
如果使用了 urllib.parse.parse_qsl,Python 程序會處理成兩個查詢參數(shù),即“a=1”和“b=2”。這種查詢參數(shù)解析的差異可能會導(dǎo)致致命的安全漏洞,比如 Django 中的 Web 緩存投毒漏洞(CVE-2021-23336)。
總結(jié)
我們介紹了 10 個 Python 安全陷阱,我們認為開發(fā)者不太了解它們。每個細微的陷阱都很容易被忽視,并在過去導(dǎo)致了線上程序的安全漏洞。
正如前文所述,安全陷阱可能出現(xiàn)在各種操作中,從處理文件、目錄、壓縮文件、URL、IP 到簡單的字符串。一種常見的情況是庫函數(shù)的使用,這些函數(shù)可能有意想不到的行為。這提醒我們一定要升級到最新版本,并仔細閱讀文檔。在 SonarSource 中,我們正在研究這些缺陷,以便將來不斷改進我們的代碼分析器。
審核編輯:湯梓紅
-
程序
+關(guān)注
關(guān)注
116文章
3766瀏覽量
80765 -
代碼
+關(guān)注
關(guān)注
30文章
4727瀏覽量
68248 -
python
+關(guān)注
關(guān)注
55文章
4774瀏覽量
84386
發(fā)布評論請先 登錄
相關(guān)推薦
評論