ContentsThe library

How a Password Is Stored

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.

FIG 1One typed password, and everywhere it stops
Twelve steps, and the password is handled correctly in exactly one of them. The branch through validation and error reporting is the common path to a real incident, because it is a path nobody designs: it happens when something goes wrong, which is the moment everybody is recording as much as possible.

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.

FIG 2Where it rests, and how bad each one is
usually present, 0 or 1retention in months, 0 thow many people can readeffort to close, 0 to 3
proxy access logs1221
application logs from de1331
error reports sent to a 1331
performance traces with 1232
crash dumps of a running0322
message queues holding t1112
database backups of a ba0333
analytics or session rep1221
The first marked cell is the error report: long retention, wide readership, and almost always present because the library is doing its job. The second is the backup, which is the one that cannot be fixed by changing code, because a backup from before you noticed still contains whatever the column contained. Every row here outlives the login it came from.
FIG 3How long a password stays, once it lands somewhere
204060800the intended lifetime, a few milliseconds7proxy access logs, typical rotation14a message queue with a long dead-letter hold30application logs in a search tool90error reports at a vendor, and backups beyond thisdays a copy survives after the login
The intended lifetime is at the far left and is invisible at this scale. Everything else on the line is a copy that exists because something recorded it, and the two marks on the right are outside your infrastructure and outside your retention controls.

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 contents

This 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

The rest of this course

  1. 01The Only Thing the Table May Hold
  2. 02The Number That Makes the Rest Necessaryopening only
  3. 03Two Rows That Look the Sameopening only
  4. 04The One Place Slowness Is the Featureopening only
  5. 05A Quarter of a Second, Ten Thousand Times at Onceopening only
  6. 06The Comparison That Tells You How Close You Wereopening only
  7. 07The Stored Row Was Never the Only Copyyou are here
  8. 08Upgrading Something You Cannot Readopening only

Read alongside