Part of the repository modernization effort.
Five defects. Each one is small. Each one is independent of the Python 2 cleanup.
Defect 1: assert guards the wire protocol
bmemcached/protocol.py:253:
assert magic == self.MAGIC['response']
This is the only check on a memcached response header.
Two problems:
python -O strips the assert. The client then processes a corrupt stream as valid data.
- The assert raises a bare
AssertionError. The surrounding except socket.error does not catch it. The header is read. The body is not read. The socket stays open in a desynchronized state. The next call on that thread reads the leftover body as a header.
Failure case: a proxy sends a malformed header. The client wedges. It does not disconnect and retry.
Fix: raise a MemcachedException instead. Call self.disconnect() before you raise.
Defect 2: assert guards input validation
bmemcached/protocol.py:670:
assert cas != 0, '0 is an invalid CAS value'
python -O strips this assert. cas(key, value, 0, ...) then reaches _set_add_replace('set', ..., cas=0). The wire protocol reads cas=0 as "no CAS check". The call becomes an unconditional overwrite. The caller asked for a compare-and-swap.
bmemcached/client/mixin.py:71:
assert servers, "No memcached servers supplied"
python -O strips this assert too. An empty server list passes. The failure appears later with an unclear message.
Fix: raise ValueError in both places.
Defect 3: MemcachedException has no usable message
bmemcached/exceptions.py:1-4:
class MemcachedException(Exception):
def __init__(self, message, code):
self.message = message
self.code = code
__init__ never calls super().__init__(message). The class defines no __str__. BaseException.__new__ still sets self.args = (message, code).
Verified:
>>> str(MemcachedException('boom', 1))
"('boom', 1)"
Every caller and every log line that calls str(exc) gets a tuple repr, not the message. AuthenticationNotSupported and InvalidCredentials inherit the same defect.
Fix: add __str__. Return the message and the code.
Defect 4: __str__ prints the SASL password
bmemcached/protocol.py:129-130:
def __str__(self):
return "{}_{}_{}".format(self.server, self._username, self._password)
The plaintext password reaches any log line, any repr() of a server list, and any traceback that prints a Protocol object.
Fix: remove the password from the string, or mask it.
Defect 5: fragile circular import
bmemcached/client/__init__.py:1-4:
from bmemcached.client.constants import SOCKET_TIMEOUT
from .replicating import ReplicatingClient
from .distributed import DistributedClient
bmemcached/client/distributed.py:4:
from bmemcached.client import SOCKET_TIMEOUT
distributed.py reads the name back off the partly initialized package module. The import works only because of the line order in __init__.py. A reorder raises ImportError at import time. No test catches this.
bmemcached/client/mixin.py:3 already uses the safe form.
Fix: change distributed.py:4 to from bmemcached.client.constants import SOCKET_TIMEOUT.
Acceptance criteria
Files to change
bmemcached/protocol.py, bmemcached/exceptions.py, bmemcached/client/mixin.py, bmemcached/client/distributed.py, test/test_errors.py, test/test_auth.py.
Order
Independent of the Python 2 cleanup issues. Do this issue before the typing issue. It changes the shape of MemcachedException.
Part of the repository modernization effort.
Five defects. Each one is small. Each one is independent of the Python 2 cleanup.
Defect 1:
assertguards the wire protocolbmemcached/protocol.py:253:This is the only check on a memcached response header.
Two problems:
python -Ostrips the assert. The client then processes a corrupt stream as valid data.AssertionError. The surroundingexcept socket.errordoes not catch it. The header is read. The body is not read. The socket stays open in a desynchronized state. The next call on that thread reads the leftover body as a header.Failure case: a proxy sends a malformed header. The client wedges. It does not disconnect and retry.
Fix: raise a
MemcachedExceptioninstead. Callself.disconnect()before you raise.Defect 2:
assertguards input validationbmemcached/protocol.py:670:python -Ostrips this assert.cas(key, value, 0, ...)then reaches_set_add_replace('set', ..., cas=0). The wire protocol readscas=0as "no CAS check". The call becomes an unconditional overwrite. The caller asked for a compare-and-swap.bmemcached/client/mixin.py:71:python -Ostrips this assert too. An empty server list passes. The failure appears later with an unclear message.Fix: raise
ValueErrorin both places.Defect 3:
MemcachedExceptionhas no usable messagebmemcached/exceptions.py:1-4:__init__never callssuper().__init__(message). The class defines no__str__.BaseException.__new__still setsself.args = (message, code).Verified:
Every caller and every log line that calls
str(exc)gets a tuple repr, not the message.AuthenticationNotSupportedandInvalidCredentialsinherit the same defect.Fix: add
__str__. Return the message and the code.Defect 4:
__str__prints the SASL passwordbmemcached/protocol.py:129-130:The plaintext password reaches any log line, any
repr()of a server list, and any traceback that prints aProtocolobject.Fix: remove the password from the string, or mask it.
Defect 5: fragile circular import
bmemcached/client/__init__.py:1-4:bmemcached/client/distributed.py:4:distributed.pyreads the name back off the partly initialized package module. The import works only because of the line order in__init__.py. A reorder raisesImportErrorat import time. No test catches this.bmemcached/client/mixin.py:3already uses the safe form.Fix: change
distributed.py:4tofrom bmemcached.client.constants import SOCKET_TIMEOUT.Acceptance criteria
bmemcached/protocol.py:253raises aMemcachedExceptionon a bad magic byte. It callsself.disconnect()first.bmemcached/protocol.py:670raisesValueErrorwhencasis 0.bmemcached/client/mixin.py:71raisesValueErroron an empty server list.MemcachedExceptiondefines__str__.str(MemcachedException("boom", 1))holds the textboom.Protocol.__str__does not hold the password.bmemcached/client/distributed.py:4imports frombmemcached.client.constants.Protocol.connectionisNoneafter the call.cas(key, value, 0, ...)raisesValueError.str(MemcachedException("boom", 1))holdsboom.str(protocol_instance)does not hold the configured password.Files to change
bmemcached/protocol.py,bmemcached/exceptions.py,bmemcached/client/mixin.py,bmemcached/client/distributed.py,test/test_errors.py,test/test_auth.py.Order
Independent of the Python 2 cleanup issues. Do this issue before the typing issue. It changes the shape of
MemcachedException.