From 191e587a3c87f4ea1a0457e7e34866c720418635 Mon Sep 17 00:00:00 2001
From: admiralobvious <aferlandqc@gmail.com>
Date: Sun, 7 Dec 2014 14:17:28 -0500
Subject: [PATCH] version 0.2.0

implements HTTP Basic Auth and fixes issue #4.
---
 docs/conf.py                 |  4 +-
 docs/index.rst               | 11 +++++-
 examples/basic_auth/app.py   |  2 +-
 flask_simpleldap/__init__.py | 77 +++++++++++++++++++++++-------------
 setup.py                     |  2 +-
 5 files changed, 62 insertions(+), 34 deletions(-)

diff --git a/docs/conf.py b/docs/conf.py
index 6913f76..d962ebd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -63,9 +63,9 @@ copyright = u'2014, Alexandre Ferland'
 # built documents.
 #
 # The short X.Y version.
-version = '0.1.0'
+version = '0.2.0'
 # The full version, including alpha/beta/rc tags.
-release = '0.1.0'
+release = '0.2.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/docs/index.rst b/docs/index.rst
index 1e7e964..0080e37 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -73,13 +73,13 @@ directives:
 ``LDAP_USER_FIELDS``         ``list`` of fields to return when searching for a user's
                              object details. Default: ``list`` (all).
 ``LDAP_USER_OBJECT_FILTER``  The filter to use when searching for a user object.
-                             Default: '(&(objectclass=Person)(userPrincipalName={}))'
+                             Default: '(&(objectclass=Person)(userPrincipalName=%s))'
 ``LDAP_USER_GROUPS_FIELD``   The field to return when searching for a user's
                              groups. Default: 'memberOf'.
 ``LDAP_GROUP_FIELDS``        ``list`` of fields to return when searching for a group's
                              object details. Default: ``list`` (all).
 ``LDAP_GROUP_OBJECT_FILTER`` The filter to use when searching for a group object.
-                             Default: '(&(objectclass=Group)(userPrincipalName={}))'
+                             Default: '(&(objectclass=Group)(userPrincipalName=%s))'
 ``LDAP_GROUP_MEMBERS_FIELD`` The field to return when searching for a group's members.
                              Default: 'member'
 ``LDAP_LOGIN_VIEW``          Views decorated with :meth:`.login_required()` or
@@ -108,6 +108,13 @@ History
 
 Changes:
 
+- 0.2.0: December 7, 2014
+
+  - Added HTTP Basic Authentication. Thanks to OptiverTimAll on GitHub.
+  - Fix GitHub issue `#4 <https://github.com/admiralobvious/flask-simpleldap/issues/4>`_,
+    User or group queries are vulnerable to LDAP injection.
+    Make sure you update your filters to use '%s' instead of the old '{}'!
+
 - 0.1.1: September 6, 2014
 
   - Fix GitHub issue `#3 <https://github.com/admiralobvious/flask-simpleldap/issues/3>`_,
diff --git a/examples/basic_auth/app.py b/examples/basic_auth/app.py
index e8905bd..40c97a9 100644
--- a/examples/basic_auth/app.py
+++ b/examples/basic_auth/app.py
@@ -15,7 +15,7 @@ ldap = LDAP(app)
 @app.route('/')
 @ldap.basic_auth_required
 def index():
-    return "Welcome, {0}!".format(g.ldap_username)
+    return 'Welcome, {0}!'.format(g.ldap_username)
 
 if __name__ == '__main__':
     app.run()
diff --git a/flask_simpleldap/__init__.py b/flask_simpleldap/__init__.py
index f2a2328..8beb126 100644
--- a/flask_simpleldap/__init__.py
+++ b/flask_simpleldap/__init__.py
@@ -5,8 +5,9 @@ from functools import wraps
 import re
 
 import ldap
-from flask import abort, current_app, g, redirect, url_for, request
-from flask import make_response
+import ldap.filter
+from flask import abort, current_app, g, make_response, redirect, url_for, \
+    request
 
 try:
     from flask import _app_ctx_stack as stack
@@ -56,11 +57,11 @@ class LDAP(object):
         app.config.setdefault('LDAP_OBJECTS_DN', 'distinguishedName')
         app.config.setdefault('LDAP_USER_FIELDS', [])
         app.config.setdefault('LDAP_USER_OBJECT_FILTER',
-                              '(&(objectclass=Person)(userPrincipalName={0}))')
+                              '(&(objectclass=Person)(userPrincipalName=%s))')
         app.config.setdefault('LDAP_USER_GROUPS_FIELD', 'memberOf')
         app.config.setdefault('LDAP_GROUP_FIELDS', [])
         app.config.setdefault('LDAP_GROUP_OBJECT_FILTER',
-                              '(&(objectclass=Group)(userPrincipalName={0}))')
+                              '(&(objectclass=Group)(userPrincipalName=%s))')
         app.config.setdefault('LDAP_GROUP_MEMBERS_FIELD', 'member')
         app.config.setdefault('LDAP_LOGIN_VIEW', 'login')
         app.config.setdefault('LDAP_REALM_NAME', 'LDAP authentication')
@@ -111,8 +112,9 @@ class LDAP(object):
 
         conn = self.initialize
         try:
-            conn.simple_bind_s(current_app.config['LDAP_USERNAME'],
-                               current_app.config['LDAP_PASSWORD'].encode('utf-8'))
+            conn.simple_bind_s(
+                current_app.config['LDAP_USERNAME'].encode('utf-8'),
+                current_app.config['LDAP_PASSWORD'].encode('utf-8'))
             return conn
         except ldap.LDAPError as e:
             raise LDAPException(self.error(e))
@@ -152,11 +154,13 @@ class LDAP(object):
         if user is not None:
             if not dn_only:
                 fields = current_app.config['LDAP_USER_FIELDS']
-            query = current_app.config['LDAP_USER_OBJECT_FILTER'].format(user)
+            query = ldap.filter.filter_format(
+                current_app.config['LDAP_USER_OBJECT_FILTER'], (user,))
         elif group is not None:
             if not dn_only:
                 fields = current_app.config['LDAP_GROUP_FIELDS']
-            query = current_app.config['LDAP_GROUP_OBJECT_FILTER'].format(group)
+            query = ldap.filter.filter_format(
+                current_app.config['LDAP_GROUP_OBJECT_FILTER'], (group,))
         conn = self.bind
         try:
             records = conn.search_s(current_app.config['LDAP_BASE_DN'],
@@ -166,7 +170,8 @@ class LDAP(object):
             if records:
                 if dn_only:
                     if current_app.config['LDAP_OBJECTS_DN'] in records[0][1]:
-                        dn = records[0][1][current_app.config['LDAP_OBJECTS_DN']]
+                        dn = records[0][1][
+                            current_app.config['LDAP_OBJECTS_DN']]
                         return dn[0]
                 for k, v in records[0][1].items():
                     result[k] = v
@@ -183,14 +188,19 @@ class LDAP(object):
 
         conn = self.bind
         try:
-            records = conn.search_s(current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
-                                    current_app.config['LDAP_USER_OBJECT_FILTER'].format(user),
-                                    [current_app.config['LDAP_USER_GROUPS_FIELD']])
+            records = conn.search_s(
+                current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
+                ldap.filter.filter_format(
+                    current_app.config['LDAP_USER_OBJECT_FILTER'], (user,)),
+                [current_app.config['LDAP_USER_GROUPS_FIELD']])
             conn.unbind_s()
             if records:
-                if current_app.config['LDAP_USER_GROUPS_FIELD'] in records[0][1]:
-                    groups = records[0][1][current_app.config['LDAP_USER_GROUPS_FIELD']]
-                    result = [re.findall('(?:cn=|CN=)(.*?),', group)[0] for group in groups]
+                if current_app.config['LDAP_USER_GROUPS_FIELD'] in records[0][
+                    1]:
+                    groups = records[0][1][
+                        current_app.config['LDAP_USER_GROUPS_FIELD']]
+                    result = [re.findall('(?:cn=|CN=)(.*?),', group)[0] for
+                              group in groups]
                     return result
         except ldap.LDAPError as e:
             raise LDAPException(self.error(e))
@@ -204,13 +214,17 @@ class LDAP(object):
 
         conn = self.bind
         try:
-            records = conn.search_s(current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
-                                    current_app.config['LDAP_GROUP_OBJECT_FILTER'].format(group),
-                                    [current_app.config['LDAP_GROUP_MEMBERS_FIELD']])
+            records = conn.search_s(
+                current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE,
+                ldap.filter.filter_format(
+                    current_app.config['LDAP_GROUP_OBJECT_FILTER'], (group,)),
+                [current_app.config['LDAP_GROUP_MEMBERS_FIELD']])
             conn.unbind_s()
             if records:
-                if current_app.config['LDAP_GROUP_MEMBERS_FIELD'] in records[0][1]:
-                    members = records[0][1][current_app.config['LDAP_GROUP_MEMBERS_FIELD']]
+                if current_app.config['LDAP_GROUP_MEMBERS_FIELD'] in \
+                        records[0][1]:
+                    members = records[0][1][
+                        current_app.config['LDAP_GROUP_MEMBERS_FIELD']]
                     return members
         except ldap.LDAPError as e:
             raise LDAPException(self.error(e))
@@ -240,6 +254,7 @@ class LDAP(object):
             if g.user is None:
                 return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW']))
             return func(*args, **kwargs)
+
         return wrapped
 
     @staticmethod
@@ -253,20 +268,25 @@ class LDAP(object):
         user and ``flask.g.ldap_groups`` to the authenticated's user's groups
         if the credentials are acceptable.
 
-        :param list groups: List of groups that should be able to access the view
-            function.
+        :param list groups: List of groups that should be able to access the
+            view function.
         """
 
         def wrapper(func):
             @wraps(func)
             def wrapped(*args, **kwargs):
                 if g.user is None:
-                    return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW']))
+                    return redirect(
+                        url_for(current_app.config['LDAP_LOGIN_VIEW']))
+
                 match = [group for group in groups if group in g.ldap_groups]
                 if not match:
                     abort(401)
+
                 return func(*args, **kwargs)
+
             return wrapped
+
         return wrapper
 
     def basic_auth_required(self, func):
@@ -283,10 +303,11 @@ class LDAP(object):
 
         :param func: The view function to decorate.
         """
+
         def make_auth_required_response():
-            response = make_response("Authorization required", 401)
+            response = make_response('Unauthorized', 401)
             response.www_authenticate.set_basic(
-                    current_app.config['LDAP_REALM_NAME'])
+                current_app.config['LDAP_REALM_NAME'])
             return response
 
         @wraps(func)
@@ -299,12 +320,12 @@ class LDAP(object):
                 req_password = request.authorization.password
 
             if req_username is None or req_password is None:
-                current_app.logger.debug("Got a request without auth data")
+                current_app.logger.debug('Got a request without auth data')
                 return make_auth_required_response()
 
             if not self.bind_user(req_username, req_password):
-                current_app.logger.debug("User {0!r} gave wrong "
-                    "password".format(req_username))
+                current_app.logger.debug('User {0!r} gave wrong '
+                                         'password'.format(req_username))
                 return make_auth_required_response()
 
             g.ldap_username = req_username
diff --git a/setup.py b/setup.py
index e8270cc..33c5b72 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ from setuptools import setup
 
 setup(
     name='Flask-SimpleLDAP',
-    version='0.1.1',
+    version='0.2.0',
     url='https://github.com/admiralobvious/flask-simpleldap',
     license='MIT',
     author='Alexandre Ferland',
-- 
GitLab