The Password Reset Token That Landed in the Access Log

The token travelled as a query parameter, so the web server logged it, and the log was shipped to a third party, and the token was valid for an hour, and…

Share
The Password Reset Token That Landed in the Access Log. Abstract security illustration in orange and dark grey on debugly.dev

I have chased this from the exposure end and from the token end, and the truth was in the hand-off, as it usually is.

The secret in the URL has a single weak property, and here it is: the URL is logged by every layer that handles the request, by default, completely, and the logs have their own retention and their own readers.

The case was a password reset link; the mechanics generalise to any one-time credential delivered as a query parameter.

Why the token was in the URL

The reset link was the email's, and the email's was the clickable, and the clickable was the GET, and the GET was the URL, and the URL was the only channel, and the channel was the query parameter, and the parameter was the convenience, and the convenience was the no form, and the no form was the one click, and the one click was the user's experience, and the experience was the decision, and the decision was the exposure.

The alternative is the code's display, and the display is the form's entry, and the entry is the POST, and the POST is the body, and the body is the not logged, and the not logged is the fix, and the fix is the extra step, and the extra step is the trade, and the trade is the security against the convenience, and the convenience is the usual winner, and the winner is the incident.

The token is the one-time and the short-lived, and the short-lived is the mitigation, and the mitigation is the hour, and the hour is the window, and the window is the enough, and the enough is the attacker's time, and the time is the account, and the account is the incident, and the incident is the reason the short life is the not the fix.

Why the log was the worse place

The log is the copied, and the copied is the every layer, and the every layer is the server and the proxy and the CDN and the analytics, and the four are the four copies, and the copies are the retention, and the retention is the months, and the months is the beyond the token's life, and the life is the irrelevant, because the copy persists after the token expires, and the persists is the pattern, and the pattern is the attacker's learning, and the learning is the next reset's predictability.

The log is also the broadly readable, and the readable is the support and the analytics and the contractor, and the three are the audience, and the audience is the not the security team, and the not security is the exposure, and the exposure is the incident, and the incident is the reason the secret should never enter the URL, because the URL is the most logged part of the request.

This is the same placement failure as the API key in the query string that lived in every log, and the common root is the property, not the platform.

What actually fixed it

Delivered a short code and required the POST. The email carried the six-digit code, and the code was the form's entry, and the entry was the POST, and the POST was the body, and the body was the not logged, and the not logged was the fix, and the fix was the extra step, and the step was the trade worth making.

Kept the URL route but moved the token to the fragment where possible. The fragment is the not sent to the server, and the not sent is the not logged, and the not logged is the fix, and the fix is the hash, and the hash is the client's, and the client's is the discipline, because the fragment does not reach the access log.

Scrubbed the query string from the access log. The log format excluded the sensitive parameters, and the excluded was the copy's absence, and the absence was the fix, and the fix was the configuration, and the configuration was the mitigation, and the mitigation was the not the prevention, because the prevention is the placement.

Made the token single-use and invalidated on the first read. The single-use was the replay's absence, and the absence was the fix, and the fix was the consumed flag, and the flag was the discipline, because the multi-use token in the log is the reusable credential.

Purged the logs of the exposure window. The purge was the copies' removal, and the removal was the remediation, and the remediation was the fix, and the fix was the every layer, and the every layer was the discipline, because the rotation kills the token and the purge kills the copies.

If it happens again

A secret in a query parameter is copied into every access log, proxy log and analytics platform by default, and the copies outlive the token. Deliver a short code and require a POST, keep the token out of the URL, scrub sensitive parameters from the log format, make the token single-use, and purge the logs of the exposure window.

The password reset token was valid for one hour and sat in the web server's access log for six months, because the token travelled as a query parameter and the URL is the part of the request every layer records completely and by default. That is the whole pattern, and it is the part worth remembering.


Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud