[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