[英]How to safely authenticate a user using LDAP?
對於上下文:我正在開發一個 web 應用程序,用戶需要在其中進行身份驗證才能查看內部文檔。 我不需要任何有關用戶的詳細信息,也不需要特殊權限管理,兩種狀態就足夠了:session 屬於經過身份驗證的用戶(→ 文檔可以訪問)或不屬於(→ 文檔無法訪問)。 用戶通過提供用戶名和密碼進行身份驗證,我想檢查 LDAP 服務器。
我正在使用 Python 3.10 和ldap3
Python 庫。
我目前正在使用以下代碼對用戶進行身份驗證:
#!/usr/bin/env python3
import ssl
from ldap3 import Tls, Server, Connection
from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError
def is_valid(username: str, password: str) -> bool:
tls_configuration = Tls(validate=ssl.CERT_REQUIRED)
server = Server("ldaps://ldap.example.com", tls=tls_configuration)
user_dn = f"cn={username},ou=ops,dc=ldap,dc=example,dc=com"
try:
with Connection(server, user=user_dn, password=password):
return True
except (LDAPBindError, LDAPPasswordIsMandatoryError):
return False
如果要運行此代碼,可以嘗試使用FreeIPA 的項目演示 LDAP 服務器。
CERT_REQUIRED
替換為CERT_NONE
因為服務器僅提供自簽名證書(這顯然是一個安全漏洞,但需要使用此特定演示 - 我要使用的服務器使用 Let's Encrypt 證書)。"ldaps://ldap.example.com"
替換為ldaps://ipa.demo1.freeipa.org
user_dn
替換為f"uid={username},cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org"
這樣做之后,您可以嘗試運行以下命令:
>>> is_valid("admin", "Secret123")
True
>>> is_valid("admin", "Secret1234")
False
>>> is_valid("admin", "")
False
>>> is_valid("admin", None)
False
>>> is_valid("nonexistent", "Secret123")
False
上面的代碼是否可以安全地確定用戶是否提供了有效的憑據?
值得注意的是,我擔心以下特定方面:
with
語句的主體只應在綁定成功時執行,因此返回True
無需多言。 這安全嗎? 或者是否有可能綁定成功,但提供的密碼仍然被認為是錯誤的,不足以根據 web 應用程序對用戶進行身份驗證。user_dn = f"cn={username},ou=ops,dc=ldap,dc=example,dc=com"
使用不受信任的username
(直接來自 web 表單)來構建字符串。 這基本上是在尖叫 LDAP 注射。另外,當然,如果我的代碼還有其他不安全的地方,我很樂意知道它是什么。
我已經搜索了特定方面的答案。 可悲的是,我沒有找到任何確定的東西(即沒有人肯定地說我在這里做的事情是壞的還是好的),但我想提供它們作為潛在答案的起點:
Connection
object 的ldap3
文檔確實提到在與不受信任的用戶名綁定時應該使用escape_rdn
。 這與我的假設不一致,誰是對的?嘗試綁定到 LDAP 服務器是否足以驗證憑據?
從 LDAP 協議方面來看,是的,並且許多系統已經依賴此行為(例如,針對 LDAP 服務器的 Linux 操作系統級身份驗證的 pam_ldap)。 我從未聽說過任何將綁定結果推遲到另一個操作的服務器。
從 ldap3 模塊方面我會更擔心,因為在我初始化 Connection 的經驗中,在我明確調用.bind()
之前(或者除非我指定 auto_bind=True),它並沒有嘗試連接(更不用說綁定)到服務器,但是如果您的示例有效,那么我假設使用with
塊可以正確執行此操作。
在舊代碼中(它擁有一個持久連接,沒有'with')我使用過這個,但它可能已經過時了:
conn = ldap3.Connection(server, raise_exceptions=True)
conn.bind()
(對於某些應用程序,我使用 Apache 作為反向代理,它的 mod_auth_ldap 為我處理 LDAP 身份驗證,尤其是當“已通過身份驗證”就足夠了。)
我是否對注射攻擊敞開心扉? 如果是這樣,如何正確緩解它們?
嗯,有點,但不是以一種容易被利用的方式。 綁定 DN 不是一個自由格式的查詢——它只是一個看起來很奇怪的“用戶名”字段,它必須與現有條目完全匹配; 你不能在里面放通配符。
(在 LDAP 服務器的最大利益中,嚴格“綁定”操作接受的內容,因為它實際上是在完成其他任何操作之前登錄 LDAP 服務器的面向用戶的操作 - 它不僅僅是“密碼檢查”ZA394FAB54074D78 )
例如,如果您有一些用戶在 OU=Ops,一些用戶在 OU=Superops,OU=Ops,那么有人可以指定Foo,OU=Superops
作為他們的用戶名,從而導致UID=Foo,OU=Superops,OU=Ops,
如DN——但他們仍然必須為該帳戶提供正確的密碼; 他們不能在檢查另一個帳戶的密碼時欺騙服務器使用一個帳戶的權限。
但是,無論如何,很容易避免注入。 可以使用以下方法轉義 DN 組件值:
ldap3.utils.dn.escape_rdn(string)
ldap.dn.escape_dn_chars(string)
話雖如此,我不喜歡“DN 模板”方法的原因完全不同——它的用處相當有限; 它僅在您的所有帳戶都在同一個 OU(平面層次結構)下並且僅當它們以uid
屬性命名時才有效。
專門構建的 LDAP 目錄可能就是這種情況,但在典型的 Microsoft Active Directory 服務器上(或者,我相信,在某些 FreeIPA 服務器上),用戶帳戶條目以其全名( cn
屬性)命名,並且可以分散在許多 OU 中。 兩步法更常見:
uid
屬性中包含用戶名或類似屬性的任何“用戶”條目,並驗證您是否找到了一個條目; 搜索時,您確實不得不擔心 LDAP 過濾器注入攻擊,因為像foo)(uid=*
可能會產生不良結果。(但要求結果與 1 個條目完全匹配 - 而不是“至少 1 個” -也有助於減輕這種情況。)
可以使用以下方法轉義過濾器值:
ldap3.utils.conv.escape_filter_chars(string)
ldap.filter.escape_filter_chars(string)
(python-ldap 也有一個方便的包裝器ldap.filter.filter_format
,但它基本上只是the_filter % tuple(map(escape_filter_chars, args))
。)
過濾器值的 escaping 規則與 RDN 值的規則不同,因此您需要針對特定上下文使用正確的規則。 但至少與 SQL 不同,它們在任何地方都完全相同,因此您的 LDAP 客戶端模塊附帶的功能適用於任何服務器。
TLS 是否正確配置?
ldap3/core/tls.py 在我看來不錯——它使用 ssl.create_default_context() 在支持時加載系統默認 CA 證書,因此不需要額外配置。 盡管它確實實現了自定義主機名檢查,而不是依賴於 ssl 模塊的 check_hostname ,所以這有點奇怪。 (也許 LDAP-over-TLS 規范定義了與通常的 HTTP-over-TLS 稍有不兼容的通配符匹配規則。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.