The API Key in the Query String That Lived in Every Log
A key passed as a query parameter is copied into access logs, proxy logs, referrer headers and browser history, and rotation does not unlog it.
The audit found the API key in the access logs, which was expected, and in the proxy logs, the CDN logs, the analytics referrer, and three browsers' history, which was the audit, and the key had never been stolen, because it had never needed to be, it was copied, politely, by every system that handles a URL, because the URL is logged by default, everywhere, and the key was in the URL, which is the decision that distributed it, and the distribution was the leak, performed by the infrastructure doing its job.
Strip the credential in the query string down and one property makes it the worst placement: the URL is the one part of the request that every layer records, because the URL is the request's identity, and identity is what logs keep.
This was a client that authenticated with a key as a query parameter, a pattern inherited from an old API, and the finding is not the API's age, it is the placement's mechanics, which are current, and general.
Why the URL is the leakiest place
The request has several parts, the method, the headers, the body, the path and query, and they are treated differently by the infrastructure. The body is rarely logged, because it is opaque and large. The headers are logged selectively, because they are known to carry credentials, and the tooling redacts them. The URL is logged completely, by default, by the web server, the proxy, the CDN, the load balancer, the monitoring, the analytics, because the URL is the request's name, and the name is what every layer records to describe its traffic, and the recording is correct, and universal, and that is the leak.
The key in the URL therefore lands in every layer's log, and the layers are many, and each layer's log has its own retention, its own readers, its own backups, so the key's copies multiply by the number of layers, and the rotation, which kills the key, does not kill the copies, which are in logs, which are the one artefact designed to survive, which is the cruelty: the credential is placed in the part of the request built to be remembered.
The referrer, and the browser's memory
The URL also leaves the request, via the Referer header, when the page that contains the URL links onward, so the key, in a browser context, is sent to the next site, which logs it as a referrer, which is the key handed to a third party, by the browser, correctly, per the referrer's design. And the browser stores the URL in its history, and the history syncs, and the synced history is the key in the sync, and the history is the artefact the user cannot rotate, which is the exposure that outlives the key's validity in the places the rotation cannot reach.
This is the ride along family from the session cookie without SameSite that rode along cross site, same mechanism, different costume.
Why the pattern persists
The query parameter key persists because it is convenient: it survives proxies that drop headers, it works in a browser link, it is easy to test with curl, and each convenience is real, and each is a reason the placement survives review, and the review's failure is that it prices the convenience and not the copies, and the copies are the incident.
The header, by contrast, is the placement the infrastructure treats as credential shaped: the Authorization header is redacted by default in most loggers, is not sent as a referrer, is not stored in history, and is not cached by the CDN, because the header's contract is secrecy, and the tooling honours the contract, which is the difference: the URL's contract is identity, logged everywhere, and the header's contract is secrecy, redacted everywhere, and the placement chooses the contract, and the contract chooses the exposure.
What actually fixed it
Move the key to the header. The Authorization header, or the API's own header, carries the key with the secrecy contract, and the loggers redact it, and the referrer omits it, and the history never sees it, and the move is one client change and one server change, and the server accepting both during the migration, which is the dual window from rotating a database password without dropping a request, applied to the placement.
Purge the logs that hold the old key. The rotation kills the key, and the purge kills the copies, and the purge is the audit's action: the access logs, the proxy logs, the CDN logs, the analytics, each purged of the window in which the key was in the URL, which is the copies removed, and the purge is the half the rotation skips, per deleting the commit did not delete the secret, and the common root is the property, not the platform.
Refuse the query parameter at the server. The server rejects, or at least does not honour, the key in the query, after the migration window, so the placement cannot regress, and the rejection is logged as an event, which is the tripwire for the client that still sends it, and the tripwire names the laggard, which is the migration's completion metric.
Treat the URL as public, always. The discipline that prevents the class: the URL is public, by construction, because every layer logs it, so no secret enters the URL, and the rule is stated in the API's design guide, which is the contract made visible, and the guide is the review's reference, which is the prevention at the design layer.
If it happens again
The URL is the part of the request every layer logs, by default, completely, so a key in the query string is copied into every log, referrer and history, and the rotation kills the key but not the copies, which are in the artefacts built to survive. Move the key to the header, purge the logs of the exposure window, refuse the query placement after migration, and treat the URL as public by construction.
The key was never stolen. It was archived, by every system that handles a URL, correctly, on every request, and the archiving is the leak. I now check for that property first, because it is where the failure actually lives.
Related reading: openssl s_client Answers Every TLS Question explores a connected angle.
Published on debugly.dev by Rohit Bhadani | Founder of Krova Cloud