简体   繁体   English

如何使用 LDAP 安全地验证用户身份?

[英]How to safely authenticate a user using LDAP?

For context: I am developing a web application where users need to authenticate to view internal documents.对于上下文:我正在开发一个 web 应用程序,用户需要在其中进行身份验证才能查看内部文档。 I neither need any detailed info on users nor special permission management, two states are sufficient: Either a session belongs to an authenticated user (→ documents can be accessed) or it does not (→ documents cannot be accessed).我不需要任何有关用户的详细信息,也不需要特殊权限管理,两种状态就足够了:session 属于经过身份验证的用户(→ 文档可以访问)或不属于(→ 文档无法访问)。 A user authenticates by providing a username and a password, which I want to check against an LDAP server.用户通过提供用户名和密码进行身份验证,我想检查 LDAP 服务器。

I am using Python 3.10 and the ldap3 Python library.我正在使用 Python 3.10 和ldap3 Python 库。

The code编码

I am currently using the following code to authenticate a user:我目前正在使用以下代码对用户进行身份验证:

#!/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

Demo instance演示实例

If you want to run this code, you could try using the FreeIPA's project demo LDAP server .如果要运行此代码,可以尝试使用FreeIPA 的项目演示 LDAP 服务器

  • Replace CERT_REQUIRED with CERT_NONE because the server only provides a self-signed cert (this obviously is a security flaw, but required to use this particular demo – the server I want to use uses a Let's Encrypt certificate).CERT_REQUIRED替换为CERT_NONE因为服务器仅提供自签名证书(这显然是一个安全漏洞,但需要使用此特定演示 - 我要使用的服务器使用 Let's Encrypt 证书)。
  • Replace "ldaps://ldap.example.com" with ldaps://ipa.demo1.freeipa.org"ldaps://ldap.example.com"替换为ldaps://ipa.demo1.freeipa.org
  • Replace the user_dn with f"uid={username},cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org"user_dn替换为f"uid={username},cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org"

After doing so, you could try running the following commands:这样做之后,您可以尝试运行以下命令:

>>> is_valid("admin", "Secret123")
True
>>> is_valid("admin", "Secret1234")
False
>>> is_valid("admin", "")
False
>>> is_valid("admin", None)
False
>>> is_valid("nonexistent", "Secret123")
False

My question(s)我的问题

Does the code above safely determine if a user has provided valid credentials?上面的代码是否可以安全地确定用户是否提供了有效的凭据?

Notably, I am concerned about the following particular aspects:值得注意的是,我担心以下特定方面:

  1. Is attempting to bind to the LDAP server enough to verify credentials?尝试绑定到 LDAP 服务器是否足以验证凭据?
    • The body of the with statement should only be executed if binding was successful and therefore returns True without further ado. with语句的主体只应在绑定成功时执行,因此返回True无需多言。 Is this safe?这安全吗? Or could it be possible that binding succeeds but the password provided would still be considered wrong and not sufficient to authenticate the user against the web app.或者是否有可能绑定成功,但提供的密码仍然被认为是错误的,不足以根据 web 应用程序对用户进行身份验证。
  2. Am I opening myself up to injection attacks?我是否对注射攻击敞开心扉? If so, how to properly mitigate them?如果是这样,如何正确缓解它们?
    • user_dn = f"cn={username},ou=ops,dc=ldap,dc=example,dc=com" uses the untrusted username (that came directly from the web form) to build a string. user_dn = f"cn={username},ou=ops,dc=ldap,dc=example,dc=com"使用不受信任username (直接来自 web 表单)来构建字符串。 That basically screams LDAP injection.这基本上是在尖叫 LDAP 注射。
  3. Is TLS properly configured? TLS 是否正确配置?
    • The connection should use modern TLS encryption and verify the certificate presented by the server, just like a normal browser would do.连接应该使用现代 TLS 加密并验证服务器提供的证书,就像普通浏览器一样。

Also, of course, if there is anything else unsafe about my code, I'd be happy to know what it is.另外,当然,如果我的代码还有其他不安全的地方,我很乐意知道它是什么。

Resources I've already found我已经找到的资源

I've already searched for answers to the particular aspects.我已经搜索了特定方面的答案。 Sadly, I have found nothing definite (ie no one definitely saying something I do here is bad or good), but I wanted to provide them as a starting point for a potential answer:可悲的是,我没有找到任何确定的东西(即没有人肯定地说我在这里做的事情是坏的还是好的),但我想提供它们作为潜在答案的起点:

  1. Probably yes.大概是。
  2. Probably not, so no mitigation is needed.可能不会,所以不需要缓解。
    • There are a few questions on LDAP injection (like “How to prevent LDAP-injection in ldap3 for python3” ) but they always only mention filtering and search, not binding.有一些关于 LDAP 注入的问题(比如“如何防止 ldap3 中的 LDAP-injection for python3” )但他们总是只提到过滤和搜索,而不是绑定。
    • The OWASP Cheat Sheet on LDAP Injection mentions enabling bind authentication as a way to mitigate LDAP injection when filtering, but say nothing about sanitization needed for the bind DN. LDAP 注入的 OWASP 备忘单提到启用绑定身份验证作为一种在过滤时减轻 LDAP 注入的方法,但没有提及绑定 DN 所需的清理。
    • I suppose you could even argue that this scenario is not susceptible to injection attacks, because we are indeed processing untrusted input, but only where untrusted input is expected .我想你甚至可以争辩说这种情况不容易受到注入攻击,因为我们确实在处理不受信任的输入,但只在预期不受信任的输入的地方 Anyone can type anything into a login form, but they can also put anything into a request to bind to an LDAP server (without even bothering with the web app).任何人都可以在登录表单中输入任何内容,但他们也可以在请求中输入任何内容以绑定到 LDAP 服务器(甚至无需使用 web 应用程序)。 As long as I don't put untrusted input somewhere where trusted input is expected (eg using a username in a filter query after binding with an LDAP admin account), I should be safe.只要我不将不受信任的输入放在预期受信任输入的地方(例如,在与 LDAP 管理员帐户绑定后在过滤器查询中使用用户名),我应该是安全的。
    • However, the ldap3 documentation of the Connection object does mention one should use escape_rdn when binding with an untrusted username.但是, Connection object 的ldap3文档确实提到在与不受信任的用户名绑定时应该使用escape_rdn This is at odds with my suppositions, who's right?这与我的假设不一致,谁是对的?
  3. Probably yes.大概是。
    • At least an error was thrown when I tried to use this code with a server that only presented a self-signed certificate, so I suppose I should be safe.当我尝试将此代码与仅提供自签名证书的服务器一起使用时,至少引发了一个错误,所以我想我应该是安全的。

Is attempting to bind to the LDAP server enough to verify credentials?尝试绑定到 LDAP 服务器是否足以验证凭据?

From the LDAP protocol side, yes, and many systems already rely on this behavior (eg pam_ldap for Linux OS-level authentication against an LDAP server).从 LDAP 协议方面来看,是的,并且许多系统已经依赖此行为(例如,针对 LDAP 服务器的 Linux 操作系统级身份验证的 pam_ldap)。 I've never heard of any server where the bind result would be deferred until another operation.我从未听说过任何将绑定结果推迟到另一个操作的服务器。

From the ldap3 module side I'd be more worried, as in my experience initializing a Connection did not attempt to connect – much less bind – to the server until I explicitly called .bind() (or unless I specified auto_bind=True), but if your example works then I assume that using a with block does this correctly.从 ldap3 模块方面我会更担心,因为在我初始化 Connection 的经验中,在我明确调用.bind()之前(或者除非我指定 auto_bind=True),它并没有尝试连接(更不用说绑定)到服务器,但是如果您的示例有效,那么我假设使用with块可以正确执行此操作。

In old code (which holds a persistent connection, no 'with') I've used this, but it may be outdated:在旧代码中(它拥有一个持久连接,没有'with')我使用过这个,但它可能已经过时了:

conn = ldap3.Connection(server, raise_exceptions=True)
conn.bind()

(For some apps I use Apache as a reverse proxy and its mod_auth_ldap handles LDAP authentication for me, especially when "is authenticated" is sufficient.) (对于某些应用程序,我使用 Apache 作为反向代理,它的 mod_auth_ldap 为我处理 LDAP 身份验证,尤其是当“已通过身份验证”就足够了。)

Am I opening myself up to injection attacks?我是否对注射攻击敞开心扉? If so, how to properly mitigate them?如果是这样,如何正确缓解它们?

Well, kind of, but not in a way that would be easily exploitable.嗯,有点,但不是以一种容易被利用的方式。 The bind DN is not a free-form query – it's only a weird-looking "user name" field and it must exactly match an existing entry;绑定 DN 不是一个自由格式的查询——它只是一个看起来很奇怪的“用户名”字段,它必须与现有条目完全匹配; you can't put wildcards in it.你不能在里面放通配符。

(It's in the LDAP server's best interests to be strict about what the "bind" operation accepts, because it's literally the user-facing operation for logging into an LDAP server before anything else is done – it's not just a "password check" function.) (在 LDAP 服务器的最大利益中,严格“绑定”操作接受的内容,因为它实际上是在完成其他任何操作之前登录 LDAP 服务器的面向用户的操作 - 它不仅仅是“密码检查”ZA394FAB54074D78 )

For example, if you have some users at OU=Ops and some at OU=Superops,OU=Ops, then someone could specify Foo,OU=Superops as their username resulting in UID=Foo,OU=Superops,OU=Ops, as the DN – but they'd still have to provide the correct password for that account anyway;例如,如果您有一些用户在 OU=Ops,一些用户在 OU=Superops,OU=Ops,那么有人可以指定Foo,OU=Superops作为他们的用户名,从而导致UID=Foo,OU=Superops,OU=Ops,如DN——但他们仍然必须为该帐户提供正确的密码; they cannot trick the server into using one account's privileges while checking another account's password.他们不能在检查另一个帐户的密码时欺骗服务器使用一个帐户的权限。

However, it's easy to avoid injection regardless.但是,无论如何,很容易避免注入。 DN component values can be escaped using:可以使用以下方法转义 DN 组件值:

  • ldap3: ldap3.utils.dn.escape_rdn(string) ldap3: ldap3.utils.dn.escape_rdn(string)
  • python-ldap: ldap.dn.escape_dn_chars(string) python-ldap: ldap.dn.escape_dn_chars(string)

That being said, I dislike "DN template" approach for a completely different reason – its rather limited usefulness;话虽如此,我不喜欢“DN 模板”方法的原因完全不同——它的用处相当有限; it only works when all of your accounts are under the same OU (flat hierarchy) and only when they're named after the uid attribute.它仅在您的所有帐户都在同一个 OU(平面层次结构)下并且仅当它们以uid属性命名时才有效。

That may be the case for a purpose-built LDAP directory, but on a typical Microsoft Active Directory server (or, I believe, on some FreeIPA servers as well) the user account entries are named after their full name (the cn attribute) and can be scattered across many OUs.专门构建的 LDAP 目录可能就是这种情况,但在典型的 Microsoft Active Directory 服务器上(或者,我相信,在某些 FreeIPA 服务器上),用户帐户条目以其全名cn属性)命名,并且可以分散在许多 OU 中。 A two-step approach is more common:两步法更常见:

  1. Bind using your app's service credentials, then search the directory for any "user" entries that have the username in their uid attribute, or similar, and verify that you found exactly one entry;使用您的应用程序的服务凭据进行绑定,然后在目录中搜索uid属性中包含用户名或类似属性的任何“用户”条目,并验证您是否找到了一个条目;
  2. Unbind (optional?), then bind again with the user's found DN and the provided password.取消绑定(可选?),然后使用用户找到的 DN 和提供的密码再次绑定。

When searching, you do have to worry about LDAP filter injection attacks a bit more, as a username like foo)(uid=* might give undesirable results. (But requiring the results to match exactly 1 entry – not "at least 1" – helps with mitigating this as well.)搜索时,您确实不得不担心 LDAP 过滤器注入攻击,因为像foo)(uid=*可能会产生不良结果。(但要求结果与 1 个条目完全匹配 - 而不是“至少 1 个” -也有助于减轻这种情况。)

Filter values can be escaped using:可以使用以下方法转义过滤器值:

  • ldap3: ldap3.utils.conv.escape_filter_chars(string) ldap3: ldap3.utils.conv.escape_filter_chars(string)
  • python-ldap: ldap.filter.escape_filter_chars(string) python-ldap: ldap.filter.escape_filter_chars(string)

(python-ldap also has a convenient wrapper ldap.filter.filter_format around this, but it's basically just the_filter % tuple(map(escape_filter_chars, args)) .) (python-ldap 也有一个方便的包装器ldap.filter.filter_format ,但它基本上只是the_filter % tuple(map(escape_filter_chars, args)) 。)

The escaping rules for filter values are different from those for RDN values, so you need to use the correct one for the specific context.过滤器值的 escaping 规则与 RDN 值的规则不同,因此您需要针对特定上下文使用正确的规则。 But at least unlike SQL, they are exactly the same everywhere, so the functions that come with your LDAP client module will work with any server.但至少与 SQL 不同,它们在任何地方都完全相同,因此您的 LDAP 客户端模块附带的功能适用于任何服务器。

Is TLS properly configured? TLS 是否正确配置?

ldap3/core/tls.py looks good to me – it uses ssl.create_default_context() when supported, loads the system default CA certificates, so no extra configuration should be needed. ldap3/core/tls.py 在我看来不错——它使用 ssl.create_default_context() 在支持时加载系统默认 CA 证书,因此不需要额外配置。 Although it does implement custom hostname checking instead of relying on the ssl module's check_hostname so that's a bit weird.尽管它确实实现了自定义主机名检查,而不是依赖于 ssl 模块的 check_hostname ,所以这有点奇怪。 (Perhaps the LDAP-over-TLS spec defines wildcard matching rules that are slightly incompatible with the usual HTTP-over-TLS ones.) (也许 LDAP-over-TLS 规范定义了与通常的 HTTP-over-TLS 稍有不兼容的通配符匹配规则。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM