For context: I am developing a web application where users need to authenticate to view internal documents. 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). A user authenticates by providing a username and a password, which I want to check against an LDAP server.
I am using Python 3.10 and the ldap3
Python library.
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.
- Replace
CERT_REQUIRED
withCERT_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). - Replace
"ldaps://ldap.example.com"
withldaps://ipa.demo1.freeipa.org
- Replace the
user_dn
withf"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:
- Is attempting to bind to the LDAP server enough to verify credentials?
- The body of the
with
statement should only be executed if binding was successful and therefore returnsTrue
without further ado. 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.
- The body of the
- 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 untrustedusername
(that came directly from the web form) to build a string. That basically screams LDAP injection.
- Is TLS properly configured?
- The connection should use modern TLS encryption and verify the certificate presented by the server, just like a normal browser would do.
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 (i.e. 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:
- Probably yes.
- “How to bind (authenticate) a user with ldap3 in python3” uses a similar code snippet to bind, and no one explicitly says that that's bad.
- Auth0 uses this method in their blog post “Using LDAP and Active Directory with C# 101” and they probably know what they're doing.
- 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.
- 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.
- 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). As long as I don't put untrusted input somewhere where trusted input is expected (e.g. using a username in a filter query after binding with an LDAP admin account), I should be safe.
- However, the
ldap3
documentation of theConnection
object does mention one should useescape_rdn
when binding with an untrusted username. This is at odds with my suppositions, who's right?
- 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.