[Checkins] SVN: z3c.authenticator/trunk/ Security Fix: move camefrom url to a session variable instead of exposing

Roger Ineichen roger at projekt01.ch
Sun Oct 18 00:41:54 EDT 2009


Log message for revision 105125:
  Security Fix: move camefrom url to a session variable instead of exposing
  the url in the login form. Because the camefrom url is built at server side
  based on local information and will always only use internal traversal names.
  Exposing this camefrom query in the login url gives others only a point to
  attack because it could be simply set by a unfriendly domain with a custom
  url. This is much better since such a unfriendly 3rd party domain url doesn't
  get redirected by default based on the changes in zope.publisher's redirect
  method. (zope.publisher 3.9.3 does only redirect to urls located in the same
  domain by default)
    
  Remove all camefrom widgets and queries in our custom forms if you use any.
  You can just set and get the camefrom session variable in your custom forms
  if you need to.
  

Changed:
  U   z3c.authenticator/trunk/CHANGES.txt
  U   z3c.authenticator/trunk/setup.py
  U   z3c.authenticator/trunk/src/z3c/authenticator/browser/login.py
  U   z3c.authenticator/trunk/src/z3c/authenticator/credential.py
  U   z3c.authenticator/trunk/src/z3c/authenticator/interfaces.py

-=-
Modified: z3c.authenticator/trunk/CHANGES.txt
===================================================================
--- z3c.authenticator/trunk/CHANGES.txt	2009-10-18 03:51:48 UTC (rev 105124)
+++ z3c.authenticator/trunk/CHANGES.txt	2009-10-18 04:41:53 UTC (rev 105125)
@@ -2,10 +2,22 @@
 CHANGES
 =======
 
-0.7.2 (unreleased)
+0.8.0 (unreleased)
 ------------------
 
-- ...
+- Security Fix: move camefrom url to a session variable instead of exposing
+  the url in the login form. Because the camefrom url is built at server side
+  based on local information and will always only use internal traversal names.
+  Exposing this camefrom query in the login url gives others only a point to
+  attack because it could be simply set by a unfriendly domain with a custom
+  url. This is much better since such a unfriendly 3rd party domain url doesn't
+  get redirected by default based on the changes in zope.publisher's redirect
+  method. (zope.publisher 3.9.3 does only redirect to urls located in the same
+  domain by default)
+  
+  Remove all camefrom widgets and queries in our custom forms if you use any.
+  You can just set and get the camefrom session variable in you rcustom forms
+  if you need to.
 
 
 0.7.1 (2009-08-19)

Modified: z3c.authenticator/trunk/setup.py
===================================================================
--- z3c.authenticator/trunk/setup.py	2009-10-18 03:51:48 UTC (rev 105124)
+++ z3c.authenticator/trunk/setup.py	2009-10-18 04:41:53 UTC (rev 105125)
@@ -23,7 +23,7 @@
 
 setup (
     name='z3c.authenticator',
-    version='0.7.2dev',
+    version='0.8.0dev',
     author = "Roger Ineichen and the Zope Community",
     author_email = "zope-dev at zope.org",
     description = "IAuthentication implementation for for Zope3",

Modified: z3c.authenticator/trunk/src/z3c/authenticator/browser/login.py
===================================================================
--- z3c.authenticator/trunk/src/z3c/authenticator/browser/login.py	2009-10-18 03:51:48 UTC (rev 105124)
+++ z3c.authenticator/trunk/src/z3c/authenticator/browser/login.py	2009-10-18 04:41:53 UTC (rev 105125)
@@ -10,12 +10,13 @@
 __docformat__ = "reStructuredText"
 
 import zope.component
+from zope.authentication.interfaces import IAuthentication
+from zope.authentication.interfaces import ILogout
+from zope.authentication.interfaces import IUnauthenticatedPrincipal
 from zope.publisher.browser import BrowserPage
+from zope.session.interfaces import ISession
+from zope.site import hooks
 from zope.traversing.browser import absoluteURL
-from zope.site import hooks
-from zope.authentication.interfaces import IUnauthenticatedPrincipal
-from zope.authentication.interfaces import IAuthentication
-from zope.authentication.interfaces import ILogout
 
 from z3c.form.interfaces import HIDDEN_MODE
 from z3c.form.interfaces import IWidgets
@@ -30,7 +31,9 @@
 
 
 class LoginForm(form.Form):
-    """Login form."""
+    """Login form without prefix in form and widget which works with session
+    credentail plugin out of the box.
+    """
 
     template = getPageTemplate()
     layout = getLayoutTemplate()
@@ -39,39 +42,36 @@
     nextURL = None
     prefix = ''
 
-    def getCameFrom(self):
-        camefrom = self.request.get('camefrom', None)
-        if camefrom is None:
-            site = hooks.getSite()
-            camefrom = '%s/index.html' % absoluteURL(site, self.request)
-        return camefrom
+    @property
+    def message(self):
+        if IUnauthenticatedPrincipal.providedBy(self.request.principal):
+            return _(u'Please provide Login Information')
+        return u''
 
     def updateWidgets(self):
         self.widgets = zope.component.getMultiAdapter(
             (self, self.request, self.getContent()), IWidgets)
+        # the session credential use input fields without prefixes
         self.widgets.prefix = ''
         self.widgets.ignoreContext = True
         self.widgets.ignoreReadonly = True
         self.widgets.update()
-        self.widgets['camefrom'].mode = HIDDEN_MODE
-        self.widgets['camefrom'].value = self.getCameFrom()
 
-    @property
-    def message(self):
-        if IUnauthenticatedPrincipal.providedBy(self.request.principal):
-            return _(u'Please provide Login Information')
-        return u''
-
     @button.buttonAndHandler(_('Log-in'))
     def handleLogin(self, action):
         """Handle the subscribe action will register and login a user."""
         if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
-            data, errors = self.widgets.extract()
-            self.nextURL = data['camefrom']
+            session = ISession(self.request, None)
+            sessionData = session.get('z3c.authenticator.credential.session')
+            if sessionData is not None and sessionData.get('camefrom'):
+                self.nextURL = sessionData['camefrom']
+                sessionData['camefrom'] = None
 
     def __call__(self):
         self.update()
         if self.nextURL is not None:
+            # the redirect method will prevent us to redirect to a 3rd party
+            # domains since zope.publisher version 3.9.3 
             self.request.response.redirect(self.nextURL)
             return ""
         else:

Modified: z3c.authenticator/trunk/src/z3c/authenticator/credential.py
===================================================================
--- z3c.authenticator/trunk/src/z3c/authenticator/credential.py	2009-10-18 03:51:48 UTC (rev 105124)
+++ z3c.authenticator/trunk/src/z3c/authenticator/credential.py	2009-10-18 04:41:53 UTC (rev 105125)
@@ -20,6 +20,7 @@
 import transaction
 import persistent
 from urllib import urlencode
+from urllib import quote
 
 import zope.interface
 from zope.publisher.interfaces.http import IHTTPRequest
@@ -320,8 +321,16 @@
     def challenge(self, request):
         """Challenges by redirecting to a login form.
 
-        To illustrate, we'll create a test request:
+        To illustrate how a session plugin works, we'll first setup some session
+        machinery:
+    
+          >>> from zope.app.testing import placelesssetup
+          >>> from z3c.authenticator.testing import sessionSetUp
+          >>> placelesssetup.setUp()
+          >>> sessionSetUp()
 
+        and we'll create a test request:
+
           >>> from zope.publisher.browser import TestRequest
           >>> request = TestRequest()
 
@@ -342,8 +351,16 @@
           >>> request.response.getStatus()
           302
           >>> request.response.getHeader('location')
-          'http://127.0.0.1/@@loginForm.html?camefrom=%2F'
+          'http://127.0.0.1/@@loginForm.html'
 
+        and the camefrom session contains the camefrom url which is our
+        application root by default:
+
+          >>> session = ISession(request, None)
+          >>> sessionData = session['z3c.authenticator.credential.session']
+          >>> sessionData['camefrom']
+          '/'
+
         The plugin redirects to the page defined by the loginpagename
         attribute:
 
@@ -351,8 +368,16 @@
           >>> plugin.challenge(request)
           True
           >>> request.response.getHeader('location')
-          'http://127.0.0.1/@@mylogin.html?camefrom=%2F'
+          'http://127.0.0.1/@@mylogin.html'
 
+        and the camefrom session contains the camefrom url which is our
+        application root by default:
+
+          >>> session = ISession(request, None)
+          >>> sessionData = session['z3c.authenticator.credential.session']
+          >>> sessionData['camefrom']
+          '/'
+
         It also provides the request URL as a 'camefrom' GET style parameter.
         To illustrate, we'll pretend we've traversed a couple names:
 
@@ -371,11 +396,18 @@
           >>> plugin.challenge(request)
           True
 
-        We see the 'camefrom' points to the requested URL:
+        We see the url points to the login form URL:
 
           >>> request.response.getHeader('location') # doctest: +ELLIPSIS
-          '.../@@mylogin.html?camefrom=%2Ffoo%2Fbar%2Ffolder%2Fpage+1.html%3Fq%3Dvalue'
+          'http://127.0.0.1/@@mylogin.html'
 
+        and the camefrom session contains the camefrom url:
+
+          >>> session = ISession(request, None)
+          >>> sessionData = session['z3c.authenticator.credential.session']
+          >>> sessionData['camefrom']
+          u'/foo/bar/folder/page%201.html?q=value'
+
         If the given challenge argument doesn't provide IHTTPRequest the
         result will always be False:
 
@@ -398,10 +430,14 @@
         camefrom = '/'.join([request.getURL(path_only=True)] + stack)
         if query:
             camefrom = camefrom + '?' + query
-        url = '%s/@@%s?%s' % (absoluteURL(site, request),
-                              self.loginpagename,
-                              urlencode({'camefrom': camefrom}))
+        url = '%s/@@%s' % (absoluteURL(site, request), self.loginpagename)
+        # only redirect to the login form
         request.response.redirect(url)
+        # and store the camefrom url into a session variable, then this url
+        # should not get exposed in the login form url.
+        session = ISession(request, None)
+        sessionData = session['z3c.authenticator.credential.session']
+        sessionData['camefrom'] = camefrom.replace(' ', '%20')
         return True
 
     def logout(self, request):
@@ -411,5 +447,6 @@
 
         sessionData = ISession(request)['z3c.authenticator.credential.session']
         sessionData['credentials'] = None
+        sessionData['camefrom'] = None
         transaction.commit()
         return True

Modified: z3c.authenticator/trunk/src/z3c/authenticator/interfaces.py
===================================================================
--- z3c.authenticator/trunk/src/z3c/authenticator/interfaces.py	2009-10-18 03:51:48 UTC (rev 105124)
+++ z3c.authenticator/trunk/src/z3c/authenticator/interfaces.py	2009-10-18 04:41:53 UTC (rev 105125)
@@ -534,11 +534,7 @@
         title=_(u'Password'),
         description=_(u'Your password.'))
 
-    camefrom = zope.schema.TextLine(
-        title=_(u'Camefrom'),
-        description=_(u'The url which the user came form.'))
 
-
 # queriable search interfaces
 class IQueriableAuthenticator(ISearchable):
     """Indicates the authenticator provides a search UI for principals."""



More information about the checkins mailing list