The Stored Row Was Never the Only Copy
Last timeChecking Without Leaking
A password arrives in plain text and travels through your system before it is ever hashed. Every stop it makes is a place it can stay, and most of them keep it longer than the database would.
Every place it stops
Five lessons have been spent on the stored value. Now consider the thing
that embarrasses teams who have done all of that correctly.
The password arrives in plain text. It has to: the person typed it, and
you need the actual characters to compute the value you compare against.
So there is a window, from the moment the request arrives to the moment
the one-way function runs, in which the plain password exists inside your
system.
The window is short in time and long in components. A request passes
through a load balancer, a proxy, a web framework, some middleware, a
validation layer, a controller, and perhaps a queue, and any of those can
write it down.
Logs and error reports
Two of these are far likelier than the rest, and they are likely for the
same reason: both exist to record what happened, and neither was written
by the person who thought about the login path.
A log line is added during a debugging session, with the whole request
object in it, because that was the fastest way to see the problem. It is
committed, it ships, and it writes every password to a log with ninety-day
retention that half the engineering organisation can search.
An error report is worse, because nobody wrote it at all. The error
reporting library captures request context automatically, which is its
entire value, and that context is the request body. The report leaves your
infrastructure, lands at a vendor, and is retained under their policy.
| usually present, 0 or 1 | retention in months, 0 t | how many people can read | effort to close, 0 to 3 | |
|---|---|---|---|---|
| proxy access logs | 1 | 2 | 2 | 1 |
| application logs from de | 1 | 3 | 3 | 1 |
| error reports sent to a | 1 | 3 | 3 | 1 |
| performance traces with | 1 | 2 | 3 | 2 |
| crash dumps of a running | 0 | 3 | 2 | 2 |
| message queues holding t | 1 | 1 | 1 | 2 |
| database backups of a ba | 0 | 3 | 3 | 3 |
| analytics or session rep | 1 | 2 | 2 | 1 |
Memory, transport and dumps
A third category leaks without any code recording anything, and it is the
one that cannot be fully closed.
A crash dump contains the memory of the process, including any string
still allocated. A request body sits in a queue, a proxy buffer or a
retry record. And in a managed language a string cannot be reliably
erased: it is immutable, it may have been copied during parsing, and the
garbage collector decides when the memory is reused.
So the realistic position is to shorten the window rather than to pretend
it closes. Compute the one-way value as early in the request as you
reasonably can, do not carry the plain password into any object that
outlives the handler, do not put it in a queue or retry it, and turn off
dumps that capture full memory on the service that handles logins. Where
a language offers a mutable buffer that can be overwritten, use it, and
treat that as a reduction rather than a guarantee.
And on transport: the password is in the request body, so it must be
over an encrypted connection with no plain fallback, and never in a query
string, because query strings are logged by everything by default.
The lesson stops here
1 more paragraph to go
You have read the opening. The rest of the argument, the problems that check whether it landed, and the lines worth keeping at the end all come with a plan.
The first lesson of every course in the library reads the whole way through, free, so you can see exactly what the rest of them are.
See the planThe contentsThis is the reading half
Starting the course gives you your own copy of it. Every idea on every page has problems standing under it, marked with a reason rather than a tick, and any sentence you do not believe can be opened and argued with. None of that can happen on a page nobody owns.
The contents