From 3f6a67ea301e932ad7c0e4fdf8633860baa35fe2 Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@cccv.de>
Date: Mon, 30 Aug 2021 19:55:07 +0200
Subject: [PATCH] Catch LDAPSASLPrepError on login

Ldap3 raises LDAPSASLPrepError on bind if the password contains characters
forbidden by SASLPrep (string preperation/normalization algorithm for user
names and passwords). Examples are carriage return ("\r") or newline ("\n")
characters. See #100.
---
 tests/test_session.py | 8 ++++++++
 uffd/ldap.py          | 4 ++--
 uffd/session/views.py | 4 ++--
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/tests/test_session.py b/tests/test_session.py
index f99ab1a1..312a66b4 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -87,6 +87,14 @@ class TestSession(UffdTestCase):
 		self.assertEqual(r.status_code, 200)
 		self.assertLoggedOut()
 
+	# Regression test for #100 (uncatched LDAPSASLPrepError)
+	def test_saslprep_invalid_password(self):
+		r = self.client.post(path=url_for('session.login'),
+			data={'loginname': self.test_data.get('user').get('loginname'), 'password': 'wrongpassword\n'}, follow_redirects=True)
+		dump('login_saslprep_invalid_password', r)
+		self.assertEqual(r.status_code, 200)
+		self.assertLoggedOut()
+
 	def test_wrong_user(self):
 		r = self.client.post(path=url_for('session.login'),
 							data={'loginname': 'nouser', 'password': self.test_data.get('user').get('password')},
diff --git a/uffd/ldap.py b/uffd/ldap.py
index ed31bb01..832ab508 100644
--- a/uffd/ldap.py
+++ b/uffd/ldap.py
@@ -4,7 +4,7 @@ import hashlib
 from flask import current_app, request, abort, session
 
 import ldap3
-from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError, LDAPInvalidDnError
+from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError, LDAPInvalidDnError, LDAPSASLPrepError
 
 # We import LDAPCommitError only because it is imported from us by other files. It is not needed here
 from uffd.ldapalchemy import LDAPMapper, LDAPCommitError # pylint: disable=unused-import
@@ -74,7 +74,7 @@ def test_user_bind(bind_dn, bind_pw):
 		conn = connect_and_bind_to_ldap(server, bind_dn, bind_pw)
 		if not conn:
 			return False
-	except (LDAPBindError, LDAPPasswordIsMandatoryError, LDAPInvalidDnError):
+	except (LDAPBindError, LDAPPasswordIsMandatoryError, LDAPInvalidDnError, LDAPSASLPrepError):
 		return False
 
 	conn.search(conn.user, encode_filter(current_app.config["LDAP_USER_SEARCH_FILTER"]))
diff --git a/uffd/session/views.py b/uffd/session/views.py
index cde2a3fd..a29557a7 100644
--- a/uffd/session/views.py
+++ b/uffd/session/views.py
@@ -9,7 +9,7 @@ from uffd.database import db
 from uffd.csrf import csrf_protect
 from uffd.secure_redirect import secure_local_redirect
 from uffd.user.models import User
-from uffd.ldap import ldap, test_user_bind, LDAPInvalidDnError, LDAPBindError, LDAPPasswordIsMandatoryError
+from uffd.ldap import ldap, test_user_bind, LDAPInvalidDnError, LDAPBindError, LDAPPasswordIsMandatoryError, LDAPSASLPrepError
 from uffd.ratelimit import Ratelimit, host_ratelimit, format_delay
 from uffd.session.models import DeviceLoginInitiation, DeviceLoginConfirmation
 
@@ -46,7 +46,7 @@ def login_get_user(loginname, password):
 		session['user_pw'] = password
 		try:
 			ldap.get_connection()
-		except (LDAPBindError, LDAPPasswordIsMandatoryError):
+		except (LDAPBindError, LDAPPasswordIsMandatoryError, LDAPSASLPrepError):
 			session.clear()
 			return None
 
-- 
GitLab