CVE-2012-2144: Other (NVD-CWE-Other)
value: Session fixation vulnerability in OpenStack Dashboard (Horizon) folsom-1 and 2012.1 allows remote attackers to hijack web sessions via the sessionid cookie.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
Published: 2012-06-05T22:55Z
Repository: https://github.com/openstack/horizon
Language: Python
Files changed: horizon/exceptions.py, horizon/middleware.py, horizon/tests/auth_tests.py, horizon/users.py, horizon/views/auth.py, horizon/views/auth_forms.py
Commit date: 2012-05-04 16:26:18 -0700
Commit message: Fixes lp978896 -- Session fixation security fix

Rotates session tokens on logout, and properly clears sessions
to prevent data leakage.

Change-Id: I52d03576d07b1e023a7730857156d0da1887b1df

Diff: diff --git a/horizon/exceptions.py b/horizon/exceptions.py
index 7af8a0a6..778719e3 100644
--- a/horizon/exceptions.py
+++ b/horizon/exceptions.py
@@ -201,11 +201,11 @@ def handle(request, message=None, redirect=None, ignore=False, escalate=False):
         message = message % {"exc": exc_value}
 
     if issubclass(exc_type, UNAUTHORIZED):
         if ignore:
             return NotAuthorized
-        request.session.clear()
+        request.user_logout()
         if not handled:
             LOG.debug("Unauthorized: %s" % exc_value)
             # We get some pretty useless error messages back from
             # some clients, so let's define our own fallback.
             fallback = _("Unauthorized. Please try logging in again.")
diff --git a/horizon/middleware.py b/horizon/middleware.py
index f20c1f09..f141ff36 100644
--- a/horizon/middleware.py
+++ b/horizon/middleware.py
@@ -47,10 +47,19 @@ class HorizonMiddleware(object):
         Adds the current "active" :class:`~horizon.Dashboard` and
         :class:`~horizon.Panel` to ``request.horizon``.
 
         Adds a :class:`~horizon.users.User` object to ``request.user``.
         """
+        # A quick and dirty way to log users out
+        def user_logout(request):
+            if hasattr(request, '_cached_user'):
+                del request._cached_user
+            # Use flush instead of clear, so we rotate session keys in
+            # addition to clearing all the session data
+            request.session.flush()
+        request.__class__.user_logout = user_logout
+
         request.__class__.user = users.LazyUser()
         request.horizon = {'dashboard': None, 'panel': None}
 
     def process_exception(self, request, exception):
         """
diff --git a/horizon/tests/auth_tests.py b/horizon/tests/auth_tests.py
index ba164777..9f295d0c 100644
--- a/horizon/tests/auth_tests.py
+++ b/horizon/tests/auth_tests.py
@@ -16,10 +16,12 @@
 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import time
+
 from django import http
 from django.core.urlresolvers import reverse
 from keystoneclient import exceptions as keystone_exceptions
 from mox import IsA
 
@@ -218,5 +220,55 @@ class AuthViewTests(test.TestCase):
 
         res = self.client.get(reverse('horizon:auth_logout'))
 
         self.assertRedirectsNoFollow(res, reverse('splash'))
         self.assertNotIn(KEY, self.client.session)
+
+    def test_session_fixation(self):
+        session_ids = []
+        form_data = {'method': 'Login',
+                     'region': 'http://localhost:5000/v2.0',
+                     'password': self.user.password,
+                     'username': self.user.name}
+
+        self.mox.StubOutWithMock(api, 'token_create')
+        self.mox.StubOutWithMock(api, 'tenant_list_for_token')
+        self.mox.StubOutWithMock(api, 'token_create_scoped')
+
+        aToken = self.tokens.unscoped_token
+        bToken = self.tokens.scoped_token
+
+        api.token_create(IsA(http.HttpRequest), "", self.user.name,
+                         self.user.password).AndReturn(aToken)
+        api.tenant_list_for_token(IsA(http.HttpRequest),
+                                  aToken.id).AndReturn([self.tenants.first()])
+        api.token_create_scoped(IsA(http.HttpRequest),
+                                self.tenant.id,
+                                aToken.id).AndReturn(bToken)
+
+        api.token_create(IsA(http.HttpRequest), "", self.user.name,
+                         self.user.password).AndReturn(aToken)
+        api.tenant_list_for_token(IsA(http.HttpRequest),
+                                  aToken.id).AndReturn([self.tenants.first()])
+        api.token_create_scoped(IsA(http.HttpRequest),
+                                self.tenant.id,
+                                aToken.id).AndReturn(bToken)
+        self.mox.ReplayAll()
+
+        res = self.client.get(reverse('horizon:auth_login'))
+        self.assertEqual(res.cookies.get('sessionid'), None)
+        res = self.client.post(reverse('horizon:auth_login'), form_data)
+        session_ids.append(res.cookies['sessionid'].value)
+
+        self.assertEquals(self.client.session['user_name'],
+                          self.user.name)
+        self.client.session['foobar'] = 'MY TEST VALUE'
+        res = self.client.get(reverse('horizon:auth_logout'))
+        session_ids.append(res.cookies['sessionid'].value)
+        self.assertEqual(len(self.client.session.items()), 0)
+        # Sleep for 1 second so the session values are different if
+        # using the signed_cookies backend.
+        time.sleep(1)
+        res = self.client.post(reverse('horizon:auth_login'), form_data)
+        session_ids.append(res.cookies['sessionid'].value)
+        # Make sure all 3 session id values are different
+        self.assertEqual(len(session_ids), len(set(session_ids)))
diff --git a/horizon/users.py b/horizon/users.py
index f5dcde81..6e6be8e5 100644
--- a/horizon/users.py
+++ b/horizon/users.py
@@ -57,11 +57,11 @@ def get_user_from_request(request):
                     request=request)
     except KeyError:
         # If any of those keys are missing from the session it is
         # overwhelmingly likely that we're dealing with an outdated session.
         LOG.exception("Error while creating User from session.")
-        request.session.clear()
+        request.user_logout()
         raise exceptions.NotAuthorized(_("Your session has expired. "
                                          "Please log in again."))
 
 
 class LazyUser(object):
diff --git a/horizon/views/auth.py b/horizon/views/auth.py
index b48f24a6..5120eed7 100644
--- a/horizon/views/auth.py
+++ b/horizon/views/auth.py
@@ -94,8 +94,8 @@ def switch_tenants(request, tenant_id):
     return shortcuts.redirect("horizon:auth_login")
 
 
 def logout(request):
     """ Clears the session and logs the current user out. """
-    request.session.clear()
+    request.user_logout()
     # FIXME(gabriel): we don't ship a view named splash
     return shortcuts.redirect('splash')
diff --git a/horizon/views/auth_forms.py b/horizon/views/auth_forms.py
index 28744868..2ebecfcd 100644
--- a/horizon/views/auth_forms.py
+++ b/horizon/views/auth_forms.py
@@ -75,10 +75,20 @@ class Login(forms.SelfHandlingForm):
         if len(regions) == 1:
             self.fields['region'].initial = default_region[0]
             self.fields['region'].widget = forms.widgets.HiddenInput()
 
     def handle(self, request, data):
+        if 'user_name' in request.session:
+            if request.session['user_name'] != data['username']:
+                # To avoid reusing another user's session, create a
+                # new, empty session if the existing session
+                # corresponds to a different authenticated user.
+                request.session.flush()
+        # Always cycle the session key when viewing the login form to
+        # prevent session fixation
+        request.session.cycle_key()
+
         # For now we'll allow fallback to OPENSTACK_KEYSTONE_URL if the
         # form post doesn't include a region.
         endpoint = data.get('region', None) or settings.OPENSTACK_KEYSTONE_URL
         region_name = dict(self.fields['region'].choices)[endpoint]
         request.session['region_endpoint'] = endpoint
@@ -114,11 +124,11 @@ class Login(forms.SelfHandlingForm):
                                   _('Invalid user name or password.'))
             except:
                 # If we get here we don't want to show a stack trace to the
                 # user. However, if we fail here, there may be bad session
                 # data that's been cached already.
-                request.session.clear()
+                request.user_logout()
                 exceptions.handle(request,
                                   message=_("An error occurred authenticating."
                                             " Please try again later."),
                                   escalate=True)
 

Vulnerable code:         request.session.clear()
        request.session.clear()
    request.session.clear()
                request.session.clear()
Fixed code:         request.user_logout()
        # A quick and dirty way to log users out
        def user_logout(request):
            if hasattr(request, '_cached_user'):
                del request._cached_user
            # Use flush instead of clear, so we rotate session keys in
            # addition to clearing all the session data
            request.session.flush()
        request.__class__.user_logout = user_logout

import time


    def test_session_fixation(self):
        session_ids = []
        form_data = {'method': 'Login',
                     'region': 'http://localhost:5000/v2.0',
                     'password': self.user.password,
                     'username': self.user.name}

        self.mox.StubOutWithMock(api, 'token_create')
        self.mox.StubOutWithMock(api, 'tenant_list_for_token')
        self.mox.StubOutWithMock(api, 'token_create_scoped')

        aToken = self.tokens.unscoped_token
        bToken = self.tokens.scoped_token

        api.token_create(IsA(http.HttpRequest), "", self.user.name,
                         self.user.password).AndReturn(aToken)
        api.tenant_list_for_token(IsA(http.HttpRequest),
                                  aToken.id).AndReturn([self.tenants.first()])
        api.token_create_scoped(IsA(http.HttpRequest),
                                self.tenant.id,
                                aToken.id).AndReturn(bToken)

        api.token_create(IsA(http.HttpRequest), "", self.user.name,
                         self.user.password).AndReturn(aToken)
        api.tenant_list_for_token(IsA(http.HttpRequest),
                                  aToken.id).AndReturn([self.tenants.first()])
        api.token_create_scoped(IsA(http.HttpRequest),
                                self.tenant.id,
                                aToken.id).AndReturn(bToken)
        self.mox.ReplayAll()

        res = self.client.get(reverse('horizon:auth_login'))
        self.assertEqual(res.cookies.get('sessionid'), None)
        res = self.client.post(reverse('horizon:auth_login'), form_data)
        session_ids.append(res.cookies['sessionid'].value)

        self.assertEquals(self.client.session['user_name'],
                          self.user.name)
        self.client.session['foobar'] = 'MY TEST VALUE'
        res = self.client.get(reverse('horizon:auth_logout'))
        session_ids.append(res.cookies['sessionid'].value)
        self.assertEqual(len(self.client.session.items()), 0)
        # Sleep for 1 second so the session values are different if
        # using the signed_cookies backend.
        time.sleep(1)
        res = self.client.post(reverse('horizon:auth_login'), form_data)
        session_ids.append(res.cookies['sessionid'].value)
        # Make sure all 3 session id values are different
        self.assertEqual(len(session_ids), len(set(session_ids)))
        request.user_logout()
    request.user_logout()
        if 'user_name' in request.session:
            if request.session['user_name'] != data['username']:
                # To avoid reusing another user's session, create a
                # new, empty session if the existing session
                # corresponds to a different authenticated user.
                request.session.flush()
        # Always cycle the session key when viewing the login form to
        # prevent session fixation
        request.session.cycle_key()

                request.user_logout()