Skip to content

Commit

Permalink
Use constant time string comparisons
Browse files Browse the repository at this point in the history
This uses constant time string comparisons everywhere that looked plausibly like it
might matter:

 - Plaintext password comparison
 - Digest nonce comparison
 - Digest hash comparison

Fixes miguelgrinberg#82
  • Loading branch information
brendanlong committed Apr 13, 2019
1 parent 3f743c6 commit 89a0892
Showing 1 changed file with 29 additions and 3 deletions.
32 changes: 29 additions & 3 deletions flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@
__version__ = '3.2.4'


try:
from hmac import compare_digest
except ImportError:
import os
import hmac

def compare_digest(a, b):
"""Fallback for hmac_compare.digest on Python < 2.7.7 or < 3.3
Rather than attempting to implement a constant-time comparison in
Python, which is surprisingly difficult, we instead prevent an attacker
from controlling the input by running it through a hash function first.
We also use a random salt to prevent comparing predictable hashes, to
avoid any possibility of leaking the hash through timing attacks.
"""
salt = os.urandom(8)
return (hmac.new(salt, a.encode("UTF-8")).digest()
== hmac.new(salt, b.encode("UTF-8")).digest()
and a == b)


class HTTPAuth(object):
def __init__(self, scheme=None, realm=None):
self.scheme = scheme
Expand Down Expand Up @@ -143,7 +165,8 @@ def authenticate(self, auth, stored_password):
client_password = self.hash_password_callback(username,
client_password)
return client_password is not None and \
client_password == stored_password
stored_password is not None and \
compare_digest(client_password, stored_password)


class HTTPDigestAuth(HTTPAuth):
Expand All @@ -169,7 +192,10 @@ def default_generate_nonce():
return session["auth_nonce"]

def default_verify_nonce(nonce):
return nonce == session.get("auth_nonce")
session_nonce = session.get("auth_nonce")
if nonce is None or session_nonce is None:
return False
return compare_digest(nonce, session_nonce)

def default_generate_opaque():
session["auth_opaque"] = _generate_random()
Expand Down Expand Up @@ -235,7 +261,7 @@ def authenticate(self, auth, stored_password_or_ha1):
ha2 = md5(a2.encode('utf-8')).hexdigest()
a3 = ha1 + ":" + auth.nonce + ":" + ha2
response = md5(a3.encode('utf-8')).hexdigest()
return response == auth.response
return compare_digest(response, auth.response)


class HTTPTokenAuth(HTTPAuth):
Expand Down

0 comments on commit 89a0892

Please sign in to comment.