[Pypi-checkins] r958 - trunk/pypi

richard python-checkins at python.org
Tue Aug 23 08:44:44 CEST 2011


Author: richard
Date: Tue Aug 23 08:44:44 2011
New Revision: 958

Modified:
   trunk/pypi/pkgbase_schema.sql
   trunk/pypi/store.py
   trunk/pypi/webui.py
Log:
addition of CSRF protection; thanks Capel Brunker

Modified: trunk/pypi/pkgbase_schema.sql
==============================================================================
--- trunk/pypi/pkgbase_schema.sql	(original)
+++ trunk/pypi/pkgbase_schema.sql	Tue Aug 23 08:44:44 2011
@@ -278,4 +278,12 @@
   FOREIGN KEY (name, version) REFERENCES releases ON UPDATE CASCADE ON DELETE CASCADE
 );
 
+CREATE TABLE csrf_tokens (
+  name     text REFERENCES users(name) ON DELETE CASCADE,
+  token    text,
+  end_date timestamp without time zone,
+  PRIMARY KEY(name)
+);
+
+
 commit;

Modified: trunk/pypi/store.py
==============================================================================
--- trunk/pypi/store.py	(original)
+++ trunk/pypi/store.py	Tue Aug 23 08:44:44 2011
@@ -16,6 +16,9 @@
 from distutils.version import LooseVersion
 import trove, openid2rp
 from mini_pkg_resources import safe_name
+# csrf modules
+import hmac
+from base64 import b64encode
 
 def enumerate(sequence):
     return [(i, sequence[i]) for i in range(len(sequence))]
@@ -1778,6 +1781,44 @@
         cursor = self.get_cursor()
         safe_execute(cursor, 'delete from cookies where cookie=%s', (cookie,))
 
+    # CSRF Protection
+    
+    def get_token(self, username):
+        '''Return csrf current token for user.'''
+        cursor = self.get_cursor()
+        sql = '''select token from csrf_tokens where name=%s
+                 and end_date > NOW()'''
+        safe_execute(cursor, sql, (username,))
+        token = cursor.fetchall()
+        if not token:
+            return self.create_token(username)
+        return token[0][0]
+    
+    def create_token(self, username):
+        '''Create and return a new csrf token for user.'''
+        alphanum = string.ascii_letters + string.digits
+        # dependency on cookie existence
+        cursor = self.get_cursor()
+        safe_execute(cursor, 'select cookie from cookies where name=%s',
+                (username,))
+        cookie = cursor.fetchall()[0][0]
+        # create random data 
+        rand = [random.choice(alphanum) for i in range(12)]
+        rand.append(str(int(time.time())))
+        rand.append(cookie)
+        random.shuffle(rand)
+        rand = hmac.new(''.join(random.choice(alphanum) for i in range(16)),
+                ''.join(rand),digestmod=hashlib.sha1).hexdigest()
+        rand = b64encode(rand)
+
+        # we may have a current entry which is out of date, delete
+        safe_execute(cursor, 'delete from csrf_tokens where name=%s', (username,))
+        sql = '''insert into csrf_tokens values(%s, %s,
+                 NOW()+interval \'15 minutes\')'''
+        safe_execute(cursor, sql, (username, rand)) 
+
+        return rand
+
     # OpenID
 
     def get_provider_session(self, provider):

Modified: trunk/pypi/webui.py
==============================================================================
--- trunk/pypi/webui.py	(original)
+++ trunk/pypi/webui.py	Tue Aug 23 08:44:44 2011
@@ -337,6 +337,27 @@
         template = PyPiPageTemplate(filename, template_dir)
         content = template(**context)
 
+        # dynamic insertion of CSRF token into FORMs
+        if '"POST"' in content and filename != 'pkg_edit.pt':
+            token = '<input type="hidden" name="CSRFToken" value="%s">' % (
+                    self.store.get_token(self.username),)
+            temp = content.split('\n')
+            edit = ((i, l) for i, l in enumerate(content.split('\n')) if
+                    '"POST"' in l)
+            try:
+                for index, line in edit:
+                    while not line.endswith('>'):
+                        index += 1
+                        line = temp[index]
+                    # count spaces to align entry nicely
+                    spaces = len(line.lstrip()) - len(line)
+                    temp[index] = "\n".join((line, ' ' * spaces + token)) 
+                content = '\n'.join(temp)
+            except IndexError:
+                # this should not happen with correct HTML syntax
+                # the try is 'just in case someone does something stupid'
+                pass
+
         self.handler.send_response(200, 'OK')
         if 'content-type' in options:
             self.handler.set_content_type(options['content-type'])
@@ -512,6 +533,8 @@
         if action in ('submit', ):
             if not self.authenticated:
                 raise Unauthorised
+            if self.form['CSRFToken'] != self.store.get_token(self.username):
+                raise FormError, "Form Failure; reset form submission"
             if self.store.get_otk(self.username):
                 raise Unauthorised, "Incomplete registration; check your email"
 
@@ -910,6 +933,7 @@
         if not self.authenticated:
             raise Unauthorised
         self.usercookie = self.store.create_cookie(self.username)
+        self.store.get_token(self.username)
         self.loggedin = 1
         self.home()
 
@@ -923,6 +947,8 @@
             if not (self.store.has_role('Admin', package_name) or
                     self.store.has_role('Owner', package_name)):
                 raise Unauthorised
+            if self.form['CSRFToken'] != self.store.get_token(self.username):
+                raise FormError, "Form Failure; reset form submission"
             package = '''
 <tr><th>Package Name:</th>
     <td><input type="text" readonly name="package_name" value="%s"></td>
@@ -1517,6 +1543,7 @@
             # Authenticated, but not logged in - auto-login
             self.loggedin = True
             self.usercookie = self.store.create_cookie(self.username)
+            token = self.store.get_token(self.username)
 
         # are we editing a specific entry?
         info = {}
@@ -1588,6 +1615,9 @@
                 label = "Download URL"
             elif property == '_pypi_hidden':
                 label = "Hidden"
+            elif property == 'CSRFToken':
+                field = '<input type="hidden" name="CSRFToken" value="%s">' % (
+                        token,)
             else:
                 req = ''
             w('<tr><th %s>%s:</th><td>%s</td></tr>\n'%(req, label,
@@ -1642,6 +1672,9 @@
             raise FormError, \
                 "You must supply the PKG-INFO file"
 
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
+
         # get the data
         pkginfo = self.form['pkginfo']
         if isinstance(pkginfo, FileUpload):
@@ -1743,6 +1776,9 @@
             self.validate_metadata(data)
         except ValueError, message:
             raise FormError, message
+            
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
 
         name = data['name']
         version = data['version']
@@ -1963,6 +1999,8 @@
         if not self.authenticated:
             raise Unauthorised, \
                 "You must be identified to edit package information"
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
 
         # vars
         name = self.form['name']
@@ -2092,6 +2130,8 @@
         if not self.authenticated:
             raise Unauthorised, \
                 "You must be identified to edit package information"
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
 
         # Verify protocol version
         if self.form.has_key('protocol_version'):
@@ -2244,6 +2284,8 @@
         if not self.authenticated:
             raise Unauthorised, \
                 "You must be identified to edit package information"
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
 
         # figure the package name and version
         name = version = None
@@ -2339,6 +2381,8 @@
         '''
         if not self.authenticated:
             raise Unauthorised, 'You must authenticate'
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
         self.register_form()
 
     def register_form(self, openid_fields = (), username='', email='', openid=''):
@@ -2524,6 +2568,8 @@
 
         if "key" not in self.form:
             raise FormError, "missing key"
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
 
         key = self.form['key'].splitlines()
         for line in key[1:]:
@@ -2542,6 +2588,8 @@
             raise Unauthorised
         if "id" not in self.form:
             raise FormError, "missing parameter"
+        if self.form['CSRFToken'] != self.store.get_token(self.username):
+            raise FormError, "Form Failure; reset form submission"
         try:
             id = int(self.form["id"])
         except:
@@ -2779,6 +2827,7 @@
             self.username = user['name']
             self.loggedin = self.authenticated = True
             self.usercookie = self.store.create_cookie(self.username)
+            self.store.get_token(self.username)
             return self.home()
         # Fill openid response fields into register form as hidden fields
         del qs[':action']


More information about the Pypi-checkins mailing list