summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2012-04-09 19:19:41 -0400
committerEli Collins <elic@assurancetechnologies.com>2012-04-09 19:19:41 -0400
commitce8e7d2438a3804b50e9af2712302de8d72c9f50 (patch)
tree49628955655aca237687660f94938d0652d271cc
parent34f766f4a2f11b19ce233e136e435c131531e42c (diff)
downloadpasslib-ce8e7d2438a3804b50e9af2712302de8d72c9f50.tar.gz
*all* hashes now throw PasswordSizeError if password is larger than 4096 chars; to prevent DOS issues.
-rw-r--r--CHANGES8
-rw-r--r--docs/lib/passlib.exc.rst1
-rw-r--r--passlib/exc.py22
-rw-r--r--passlib/handlers/misc.py2
-rw-r--r--passlib/handlers/mssql.py2
-rw-r--r--passlib/handlers/scram.py2
-rw-r--r--passlib/tests/utils.py13
-rw-r--r--passlib/utils/__init__.py3
-rw-r--r--passlib/utils/handlers.py10
9 files changed, 62 insertions, 1 deletions
diff --git a/CHANGES b/CHANGES
index 7e79614..8fc1cc9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -50,6 +50,14 @@ Release History
that sometimes occurred on platforms with a deviant implementation
of :func:`!os_crypt`.
+ * All hashes will now throw :exc:`~passlib.exc.PasswordSizeError`
+ if the provided password is larger than 4096 characters.
+
+ This limit should be above any reasonable password size,
+ and prevents various things including DOS abuse of hashes
+ that have an expensive password-length-dependant stage,
+ and for OS's which have a buggy :func:`!crypt.crypt` implementation.
+
CryptContext
.. currentmodule:: passlib.context
diff --git a/docs/lib/passlib.exc.rst b/docs/lib/passlib.exc.rst
index 263f754..b60ba63 100644
--- a/docs/lib/passlib.exc.rst
+++ b/docs/lib/passlib.exc.rst
@@ -11,6 +11,7 @@ may be raised by Passlib.
Exceptions
==========
.. autoexception:: MissingBackendError
+.. autoexception:: PasswordSizeError
Warnings
========
diff --git a/passlib/exc.py b/passlib/exc.py
index cb158e7..44fadd4 100644
--- a/passlib/exc.py
+++ b/passlib/exc.py
@@ -14,6 +14,28 @@ class MissingBackendError(RuntimeError):
from :class:`~passlib.utils.handlers.HasManyBackends`.
"""
+class PasswordSizeError(ValueError):
+ """Error raised if the password provided exceeds the limit set by Passlib.
+
+ Many password hashes take proportionately larger amounts of
+ time and/or memory depending on the size of the password provided.
+ This could present a potential denial of service (DOS) situation
+ if a maliciously large password was provided to the application.
+
+ Because of this, Passlib enforces a maximum of 4096 characters.
+ This error will be thrown if a password larger than
+ this is provided to any of the hashes in Passlib.
+
+ Applications wishing to use a different limit should set the
+ ``PASSLIB_MAX_PASSWORD_SIZE`` environmental variable before Passlib
+ is loaded.
+ """
+ def __init__(self):
+ ValueError.__init__(self, "password exceeds maximum allowed size")
+
+ # this also prevents a glibc crypt segfault issue, detailed here ...
+ # http://www.openwall.com/lists/oss-security/2011/11/15/1
+
#==========================================================================
# warnings
#==========================================================================
diff --git a/passlib/handlers/misc.py b/passlib/handlers/misc.py
index 38b8c25..2fb3f13 100644
--- a/passlib/handlers/misc.py
+++ b/passlib/handlers/misc.py
@@ -170,6 +170,8 @@ class plaintext(object):
@classmethod
def encrypt(cls, secret):
+ if secret and len(secret) > uh.MAX_PASSWORD_SIZE:
+ raise uh.exc.PasswordSizeError()
return to_native_str(secret, cls._hash_encoding, "secret")
@classmethod
diff --git a/passlib/handlers/mssql.py b/passlib/handlers/mssql.py
index 36bff13..4bc7e24 100644
--- a/passlib/handlers/mssql.py
+++ b/passlib/handlers/mssql.py
@@ -160,6 +160,8 @@ class mssql2000(uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
chk = self.checksum
if chk is None:
raise uh.MissingDigestError(cls)
+ if secret and len(secret) > uh.MAX_PASSWORD_SIZE:
+ raise uh.exc.PasswordSizeError()
secret = to_unicode(secret, 'utf-8', errname='secret')
result = _raw_mssql(secret.upper(), self.salt)
return consteq(result, chk[20:])
diff --git a/passlib/handlers/scram.py b/passlib/handlers/scram.py
index 5c9c65d..ab610c8 100644
--- a/passlib/handlers/scram.py
+++ b/passlib/handlers/scram.py
@@ -351,6 +351,8 @@ class scram(uh.HasRounds, uh.HasRawSalt, uh.HasRawChecksum, uh.GenericHandler):
@classmethod
def verify(cls, secret, hash, full=False):
+ if secret and len(secret) > uh.MAX_PASSWORD_SIZE:
+ raise uh.exc.PasswordSizeError()
self = cls.from_string(hash)
chkmap = self.checksum
if not chkmap:
diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py
index b3ce550..7c79975 100644
--- a/passlib/tests/utils.py
+++ b/passlib/tests/utils.py
@@ -1231,6 +1231,19 @@ class HandlerCase(TestCase):
self.assertRaises(TypeError, self.do_genhash, None, hash)
self.assertRaises(TypeError, self.do_verify, None, hash)
+ def test_63_max_password_size(self):
+ "test MAX_PASSWORD_SIZE is enforced"
+ if self.is_disabled_handler:
+ raise self.skipTest("not applicable")
+ from passlib.exc import PasswordSizeError
+ from passlib.utils import MAX_PASSWORD_SIZE
+ secret = '.' * (1+MAX_PASSWORD_SIZE)
+ config = self.do_genconfig()
+ hash = self.get_sample_hash()[1]
+ self.assertRaises(PasswordSizeError, self.do_genhash, secret, config)
+ self.assertRaises(PasswordSizeError, self.do_encrypt, secret)
+ self.assertRaises(PasswordSizeError, self.do_verify, secret, hash)
+
#==============================================================
# check identify(), verify(), genhash() against test vectors
#==============================================================
diff --git a/passlib/utils/__init__.py b/passlib/utils/__init__.py
index 24173f7..e8e5b27 100644
--- a/passlib/utils/__init__.py
+++ b/passlib/utils/__init__.py
@@ -105,6 +105,9 @@ _BEMPTY = b('')
_UEMPTY = u("")
_USPACE = u(" ")
+# maximum password size which passlib will allow; see exc.PasswordSizeError
+MAX_PASSWORD_SIZE = int(os.environ.get("PASSLIB_MAX_PASSWORD_SIZE") or 4096)
+
#=================================================================================
#decorators and meta helpers
#=================================================================================
diff --git a/passlib/utils/handlers.py b/passlib/utils/handlers.py
index 2d283c5..7c8b747 100644
--- a/passlib/utils/handlers.py
+++ b/passlib/utils/handlers.py
@@ -13,12 +13,14 @@ import os
from warnings import warn
# site
# pkg
+import passlib.exc as exc
from passlib.exc import MissingBackendError, PasslibConfigWarning, \
PasslibHashWarning
from passlib.registry import get_crypt_handler
from passlib.utils import classproperty, consteq, getrandstr, getrandbytes,\
BASE64_CHARS, HASH64_CHARS, rng, to_native_str, \
- is_crypt_handler, deprecated_function, to_unicode
+ is_crypt_handler, deprecated_function, to_unicode, \
+ MAX_PASSWORD_SIZE
from passlib.utils.compat import b, join_byte_values, bytes, irange, u, \
uascii_to_str, join_unicode, unicode, str_to_uascii
# local
@@ -442,6 +444,8 @@ class GenericHandler(object):
@classmethod
def genhash(cls, secret, config, **context):
+ if secret and len(secret) > MAX_PASSWORD_SIZE:
+ raise exc.PasswordSizeError()
self = cls.from_string(config, **context)
self.checksum = self._calc_checksum(secret)
return self.to_string()
@@ -458,6 +462,8 @@ class GenericHandler(object):
#=========================================================
@classmethod
def encrypt(cls, secret, **kwds):
+ if secret and len(secret) > MAX_PASSWORD_SIZE:
+ raise exc.PasswordSizeError()
self = cls(use_defaults=True, **kwds)
self.checksum = self._calc_checksum(secret)
return self.to_string()
@@ -467,6 +473,8 @@ class GenericHandler(object):
# NOTE: classes with multiple checksum encodings should either
# override this method, or ensure that from_string() / _norm_checksum()
# ensures .checksum always uses a single canonical representation.
+ if secret and len(secret) > MAX_PASSWORD_SIZE:
+ raise exc.PasswordSizeError()
self = cls.from_string(hash, **context)
chk = self.checksum
if chk is None: