On May 17, a security review put one string at the center of our portal: ?k=<token>.

That parameter carried the portal’s master key. Every time someone opened a shared document link, the key could land in browser history, web-server logs, and the logs of any proxy between the browser and the app. Nothing had to be actively compromised for that to be bad. We were writing the secret into systems built to retain request metadata.

The finding was tagged Vector 11. It was simple, which is why we had missed it. The portal worked. A recipient clicked a link, the document opened, and internal navigation kept working. We had treated the query parameter as a practical bootstrap mechanism, and the audit made us look at the whole request path instead of the successful screen.

The convenient route

A link arrived with ?k=<token>, the client read the value, and the server accepted it. That made a first visit frictionless and share links self-contained. It also meant the credential sat in the URL at the point where the browser, the server, and the intermediary infrastructure first saw it.

There was no mysterious production error to chase. The review followed the key from the address bar outward. The browser retained the full URL in history. The request reached the server, where standard request logging could record it. An intermediate proxy could do the same. The secret was valid, and that was the failure mode. A valid secret in the wrong transport becomes an observability artifact.

We had mentally filed the parameter next to an ordinary filter or document ID. It was an authentication credential, and the shape of the URL hid that in plain sight.

A new order of trust

We changed the server to extract an access key in a strict priority order:

1. Authorization: Bearer <token>
2. a host-only portal cookie
3. ?k=<token> query parameter, legacy only

The query path stays for now, but it comes last. Using it fires a one-time legacy-auth log event so we can find the traffic that still depends on it. The server now prefers the header over every older place the key might appear.

That logic runs in both the document-view route and the typed RPC context. The downstream document-read and share-mint operations take the access key from that trusted request context before considering legacy request inputs. Those inputs still validate during the deprecation window, because breaking active links without a migration path would create a different operational problem. The context is the source that wins.

The sequencing matters. It is easy to move the client to a bearer header and leave one server path reading an input field first. The migration then looks complete from the happy path while a less common endpoint keeps the old behavior. We wired the same precedence through the route and the RPC boundary before calling the client change finished.

Cleaning the browser side

The client change is small and does more than it looks like it does. The RPC HTTP batch link now attaches Authorization: Bearer <token> from local storage. On an initial landing where the token arrives in ?k=, the client bootstraps from it, then removes the parameter with history.replaceState before the user refreshes or follows an internal link.

We also removed the internal navigation helper that had kept adding the key back onto URLs. Without that, the landing page would have looked clean while every click recreated the leak. A fix that only changes the first render is cosmetic. The address bar, refresh behavior, and onward navigation all had to converge on header authentication.

We looked at three easier options. Leaving ?k= in place and promising tighter logs lost immediately, because the browser history problem remains even if we tune server retention. Moving only to a cookie lost as a complete solution, because the first shared-link visit still needed to hand the recipient a credential and the existing client flow already supported header authentication. Disabling share URLs entirely would remove the exposure, but it would also remove the channel recipients use to open a shared document.

So we kept one narrow exception. Newly emitted share URLs still include ?k= because a recipient has no other channel at that first handoff. That does not make query authentication safe. It is an explicit compatibility boundary. Once the page lands, the client moves the key into the normal authentication flow, removes it from the address bar, and every refresh and internal request uses the bearer header.

Two questions about one mechanism

We had been optimizing for a simple share link and judging success by whether the recipient gets the document. The security review asked where the credential gets copied before the application reads it. A shared system has to answer both.

The change touched six files, added 131 lines, and removed 36. Most of it was not cryptography. It was consistency work: request precedence, typed context, legacy validation, client bootstrap, and navigation behavior.

If a secret has to cross that first boundary in a URL, the code needs a defined moment when it stops being URL data. Ours had none. Now it does, and it is the history.replaceState call right after landing.

If you send links, check whether any of them carry a credential. Browser history, web-server logs and proxy logs can all retain it, and a feature that works for the recipient can still copy a secret into places nobody reviews.