Defend timing-based attacks against htpasswd.
diff --git a/CHANGES.txt b/CHANGES.txt
index 9724da1..b8695a1 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,12 @@
repoze.who Changelog
====================
+Unreleased
+----------
+
+
+
2.0a4 (2011-02-02)
------------------
@@ -35,6 +41,7 @@ repoze.who Changelog
otherwise need to use private methods of the API, and reach down into
its plugins.
+
2.0a3 (2010-09-30)
------------------
@@ -71,6 +78,7 @@ repoze.who Changelog
(added missing ``global_config`` argument). See
http://bugs.repoze.org/issue114
+
2.0a2 (2010-03-25)
------------------
@@ -88,6 +96,7 @@ Backward Incompatibilities
to ``debug``.
+
2.0a1 (2010-02-24)
------------------
@@ -153,6 +162,7 @@ Backward Incompatibilities
- ``verify``
+
1.0.18 (2009-11-05)
-------------------
@@ -161,6 +171,7 @@ Backward Incompatibilities
``Expires`` attributes of those cookies.
+
1.0.17 (2009-11-05)
-------------------
@@ -169,6 +180,7 @@ Backward Incompatibilities
file).
+
1.0.16 (2009-11-04)
-------------------
diff --git a/repoze/who/plugins/htpasswd.py b/repoze/who/plugins/htpasswd.py
index f21bb0e..13c418a 100644
--- a/repoze/who/plugins/htpasswd.py
+++ b/repoze/who/plugins/htpasswd.py
@@ -1,3 +1,5 @@
+import itertools
+
from zope.interface import implements
from repoze.who.interfaces import IAuthenticator
@@ -31,6 +33,7 @@ class HTPasswdPlugin(object):
'file %s' % self.filename)
return None
+ result = None
for line in f:
try:
username, hashed = line.rstrip().split(':', 1)
@@ -38,20 +41,30 @@ class HTPasswdPlugin(object):
continue
if username == login:
if self.check(password, hashed):
- return username
- return None
+ result = username
+ # Don't bail early: leaks information!!
+ return result
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__,
id(self)) #pragma NO COVERAGE
+PADDING = ' ' * 1000
+
+def _same_string(x, y):
+ match = True
+ for a, b, ignored in itertools.izip_longest(x, y, PADDING):
+ match = a == b and match
+ return match
+
def crypt_check(password, hashed):
from crypt import crypt
salt = hashed[:2]
- return hashed == crypt(password, salt)
+ return _same_string(hashed, crypt(password, salt))
def plain_check(password, hashed):
- return hashed == password
+ return _same_string(password, hashed)
+
def make_plugin(filename=None, check_fn=None):
if filename is None:
@@ -60,5 +73,3 @@ def make_plugin(filename=None, check_fn=None):
raise ValueError('check_fn must be specified')
check = resolveDotted(check_fn)
return HTPasswdPlugin(filename, check)
-
-