-
Notifications
You must be signed in to change notification settings - Fork 178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Escape username within DN correctly and remove escape_userdn
#267
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,6 +5,7 @@ | |||||||||||||
import ldap3 | ||||||||||||||
from jupyterhub.auth import Authenticator | ||||||||||||||
from ldap3.utils.conv import escape_filter_chars | ||||||||||||||
from ldap3.utils.dn import escape_rdn | ||||||||||||||
from traitlets import Bool, Int, List, Unicode, Union, UseEnum, observe, validate | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
@@ -166,20 +167,17 @@ def _validate_bind_dn_template(self, proposal): | |||||||||||||
help="List of attributes to be searched", | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
# FIXME: Use something other than this? THIS IS LAME, akin to websites restricting things you | ||||||||||||||
# can use in usernames / passwords to protect from SQL injection! | ||||||||||||||
valid_username_regex = Unicode( | ||||||||||||||
r"^[a-z][.a-z0-9_-]*$", | ||||||||||||||
config=True, | ||||||||||||||
help=""" | ||||||||||||||
Regex for validating usernames - those that do not match this regex will be rejected. | ||||||||||||||
|
||||||||||||||
This is primarily used as a measure against LDAP injection, which has fatal security | ||||||||||||||
considerations. The default works for most LDAP installations, but some users might need | ||||||||||||||
to modify it to fit their custom installs. If you are modifying it, be sure to understand | ||||||||||||||
the implications of allowing additional characters in usernames and what that means for | ||||||||||||||
LDAP injection issues. See https://www.owasp.org/index.php/LDAP_injection for an overview | ||||||||||||||
of LDAP injection. | ||||||||||||||
Regex for validating usernames - those that do not match this regex will | ||||||||||||||
be rejected. | ||||||||||||||
|
||||||||||||||
This config was primarily introduced to prevent LDAP injection | ||||||||||||||
(https://www.owasp.org/index.php/LDAP_injection), but that is since 2.0 | ||||||||||||||
being mitigated by escaping all sensitive characters when interacting | ||||||||||||||
with the LDAP server. | ||||||||||||||
""", | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
|
@@ -250,7 +248,8 @@ def _validate_bind_dn_template(self, proposal): | |||||||||||||
default_value=None, | ||||||||||||||
allow_none=True, | ||||||||||||||
help=""" | ||||||||||||||
Technical account for user lookup, if `lookup_dn` is set to True. | ||||||||||||||
DN for a technical user account allowed to search for information about | ||||||||||||||
provided username, if `lookup_dn` is set to True. | ||||||||||||||
|
||||||||||||||
If both lookup_dn_search_user and lookup_dn_search_password are None, then anonymous LDAP query will be done. | ||||||||||||||
""", | ||||||||||||||
|
@@ -282,13 +281,16 @@ def _validate_bind_dn_template(self, proposal): | |||||||||||||
False, | ||||||||||||||
config=True, | ||||||||||||||
help=""" | ||||||||||||||
If set to True, escape special chars in userdn when authenticating in LDAP. | ||||||||||||||
|
||||||||||||||
On some LDAP servers, when userdn contains chars like '(', ')', '\' authentication may fail when those chars | ||||||||||||||
are not escaped. | ||||||||||||||
Removed in 2.0, configuring this no longer has any effect. | ||||||||||||||
""", | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
@observe("escape_userdn") | ||||||||||||||
def _observe_escape_userdn(self, change): | ||||||||||||||
self.log.warning( | ||||||||||||||
"LDAPAuthenticator.escape_userdn was removed in 2.0 and no longer has any effect." | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
search_filter = Unicode( | ||||||||||||||
config=True, | ||||||||||||||
help=""" | ||||||||||||||
|
@@ -329,16 +331,13 @@ def resolve_username(self, username_supplied_by_user): | |||||||||||||
Resolves a username supplied by a user to the a user DN when lookup_dn | ||||||||||||||
is True. | ||||||||||||||
""" | ||||||||||||||
search_dn = self.lookup_dn_search_user | ||||||||||||||
if self.escape_userdn: | ||||||||||||||
search_dn = escape_filter_chars(search_dn) | ||||||||||||||
conn = self.get_connection( | ||||||||||||||
userdn=search_dn, | ||||||||||||||
userdn=self.lookup_dn_search_user, | ||||||||||||||
password=self.lookup_dn_search_password, | ||||||||||||||
) | ||||||||||||||
if not conn.bind(): | ||||||||||||||
self.log.warning( | ||||||||||||||
f"Failed to connect to LDAP server with search user '{search_dn}'" | ||||||||||||||
f"Failed to connect to LDAP server with search user '{self.lookup_dn_search_user}'" | ||||||||||||||
) | ||||||||||||||
return (None, None) | ||||||||||||||
|
||||||||||||||
|
@@ -463,17 +462,12 @@ async def authenticate(self, handler, data): | |||||||||||||
username, resolved_dn = self.resolve_username(username) | ||||||||||||||
if not username: | ||||||||||||||
return None | ||||||||||||||
if str(self.lookup_dn_user_dn_attribute).upper() == "CN": | ||||||||||||||
# Only escape commas if the lookup attribute is CN | ||||||||||||||
username = re.subn(r"([^\\]),", r"\1\,", username)[0] | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The later escape_rdn is a superset of this comma escape. So probably ok. However the if ... logic is now missing. Is it important to only do this escape at some times? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The if logic didn't make sense to me, the username should reasonably always be escaped before being passed to the LDAP server. Hmmm, i guess this could also have impacted the username chosen if use_lookup_dn_username is true as well though... Are you a user of that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think for my use case the if condition would always be true as my company uses Active Directory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, the issue I'm seeing now, is that This is because of this line of code, where ldapauthenticator/ldapauthenticator/ldapauthenticator.py Lines 642 to 647 in cea8616
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I updated the PR description with this: EDIT: Breaking changeThis PR introduced a breaking change in a edge case scenario if both...
Then, these users' resulting JupyterHub usernames would be changed from looking like |
||||||||||||||
if not bind_dn_template: | ||||||||||||||
bind_dn_template = [resolved_dn] | ||||||||||||||
|
||||||||||||||
is_bound = False | ||||||||||||||
for dn in bind_dn_template: | ||||||||||||||
userdn = dn.format(username=username) | ||||||||||||||
if self.escape_userdn: | ||||||||||||||
userdn = escape_filter_chars(userdn) | ||||||||||||||
userdn = dn.format(username=escape_rdn(username)) | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. escape_filter_chars and escape_rdn seem to escape for different things unless I'm missing something.
Is it ok to exchange them here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes i think that is right to do, because the rdn escape is for parts of a dn like here. Filter chars wasn't right to use here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a comment to this in another PR merged after this, linking to an RFC There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you are correct. You explained this at the beginning of the PR but first I didn't understand this. |
||||||||||||||
self.log.debug(f"Attempting to bind {username} with {userdn}") | ||||||||||||||
msg = "Status of user bind {username} with {userdn} : {is_bound}" | ||||||||||||||
try: | ||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the DN here isn't escaped with escape_rdn, because its a full DN rather than a part of it. The full DN include commas and equalsigns for example, and they shouldnt be escaped as they are describing structure of the DN itself, but an individual attributes value should be escaped.
We cant do that here though as we have a full DN here and not its parts.