Tres Seaver
2011-03-13 f8ef8169680239824eb5fb11c7ce5a54938aed1c
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)
-
-
2 files modified
35 ■■■■ changed files
CHANGES.txt 12 ●●●●● patch | view | raw | blame | history
repoze/who/plugins/htpasswd.py 23 ●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -1,6 +1,12 @@
repoze.who Changelog
====================
Unreleased
----------
- ``repoze.who.plugins.htpasswd``:  defend against timing-based attacks.
2.0a4 (2011-02-02)
------------------
@@ -34,6 +40,7 @@
  as a convenience for application-driven login / logout code, which would
  otherwise need to use private methods of the API, and reach down into
  its plugins.
2.0a3 (2010-09-30)
------------------
@@ -71,6 +78,7 @@
  (added missing ``global_config`` argument).  See
  http://bugs.repoze.org/issue114
2.0a2 (2010-03-25)
------------------
@@ -86,6 +94,7 @@
- Adjusted logging level for some lower-level details from ``info``
  to ``debug``.
2.0a1 (2010-02-24)
@@ -153,6 +162,7 @@
  - ``verify``
1.0.18 (2009-11-05)
-------------------
@@ -161,6 +171,7 @@
  ``Expires`` attributes of those cookies.
1.0.17 (2009-11-05)
-------------------
@@ -169,6 +180,7 @@
  file).
1.0.16 (2009-11-04)
-------------------
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 @@
                                                  'file %s' % self.filename)
                return None
        result = None
        for line in f:
            try:
                username, hashed = line.rstrip().split(':', 1)
@@ -38,20 +41,30 @@
                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 @@
        raise ValueError('check_fn must be specified')
    check = resolveDotted(check_fn)
    return HTPasswdPlugin(filename, check)