π SEC-#50: Clear password references on close and mask in repr - #59
Conversation
FernandoCelmer
left a comment
There was a problem hiding this comment.
π Code Review
Code issues found: 1
See inline comment below.
| self.password = None | ||
|
|
||
| def noop(self) -> None: | ||
| self.require() |
There was a problem hiding this comment.
[Blocking]
Problem β After close(), self.password is set to None. The connect() method calls client.login(user=self.user, password=self.password). If a user calls close() then connect() again (a common reconnection pattern), imaplib will coerce None to the string "None" and send it as the literal password, causing a confusing authentication failure.
Failure scenario β
with Email.from_env() as app:
app.sync()
# After __exit__, close() runs -> password = None
# Later, trying to reconnect:
app.connect() # login(user='me@x.com', password=None)
# -> imaplib sends 'None' as password -> auth failure
# -> confusing error: 'Authentication failed' (wrong password)More critically, sync.py and restore.py create worker connections using self._session.password. If the main session is closed while workers are still spawning, they read password=None.
Fix β Instead of nulling the password attribute, only clear the connection handle. If secure clearing is important, capture the password in a local before threading:
# Option A: Don't null password in close()
def close(self) -> None:
if self.client:
try:
self.client.logout()
finally:
self.client = None
self.mailboxes = {}
# self.password stays intact for reconnection
# Option B: Capture password before threading in sync/restore
def sync_one(name, retries=3):
password = self._session.password # capture before any close()
session = ImapClient(password=password, ...)
Summary
self.password = NoneinImapClient.close()so the password string is not retained after disconnectionself._password = NoneinSmtpClient.close()for the same reason__repr__and__str__toCredentialsdataclass that mask the password field, preventing accidental exposure in logs or tracebacksCloses #50
Test plan
pytest tests/ -x -q)repr(Credentials(...))showspassword='***'instead of the real valueNoneafter callingclose()on both IMAP and SMTP clients