Start with a check you can finish in one tab. Open DevTools Network and compare the full address bar with the document request line. The address bar may show a key after #. That same string should not appear on the request line. If it does, this is not default browser behavior. The page wrote the fragment into a query, or a script read it and stuffed it into a request.
A query and a hash are not the same layer of protection
One-time ciphertext links are often described as “the key is in the URL.” That sentence is too coarse. It stacks two different facts on top of each other. A URL can carry both a query and a fragment: ? opens the query, # opens the fragment. Both show up in the address bar, and people usually copy the whole string. HTTP does not treat them the same way.
A query becomes part of the request target. Open s.html?id=abc123 and the server, reverse proxy, CDN, and access log are designed to see id=abc123. A fragment is not part of that document request. Open s.html?id=abc123#the-key and the browser asks the server for the page with the id. Everything after the hash stays in the current tab so page scripts can read it with location.hash.
So “the key is in the URL” only answers whether a person can see the whole string. It does not answer whether the server can see the key. The part that fits a key is the fragment after the hash—if the implementation actually leaves it there, instead of stuffing it into the query because that was easier.
What the specs actually say: fragments stay on the client
This is not a private product convention. RFC 3986 section 3.5 defines a fragment as an identifier for a “secondary resource”: the hash appears, then the rest of the URI. How that fragment is interpreted depends on the retrieved document type. The client handles it. The URI scheme does not rewrite it.
RFC 9110 section 7.1 applies the same rule to HTTP: the target URI a browser derives does not include a fragment, because fragment identifiers are for the client. When the browser requests the document, the request line should not contain the hash or anything after it.
Referer follows the same cut. RFC 9110 says a user agent that generates Referer must not include a fragment or userinfo. MDN’s Referer page matches that: the header may carry origin, path, and query, not a # fragment. The W3C Referrer Policy empties the fragment first when it turns a URL into a referrer, then decides whether path and query still remain.
Those three documents prove one thing: a spec-compliant browser will not write the key after the hash into the HTTP request line or into Referer. They do not prove that page scripts, extensions, or the text you paste somewhere else will strip it too.
If the key sits in the query, the logs see it in the clear
Writing the key as ?id=abc123&key=... is simpler to ship. The server can fetch ciphertext and decrypt it from the same query. That is common on “encrypt online” pages that want one request to do everything. It is the opposite of “the host should never see the key.”
A query appears on the request line. It also lands in most access logs, reverse-proxy logs, and some CDN reports. Once the key is in the query, “we only store ciphertext” is already false at the log layer. An operator who opens that day’s access log has the second half needed to decrypt.
Someone will suggest moving the key into a POST body instead. That can keep it out of URL logs. The key still left the browser and arrived on the machine you called “zero knowledge.” The fragment’s value is the opposite: the key does not have to leave as an HTTP field at all. The page reads location.hash in this tab.
| Where you put it | Can a person see it? | Can this HTTP request see it? |
|---|---|---|
Query after ? |
Yes, in the address bar and in copied text | Yes. Request line, proxies, and access logs record it |
Fragment after # |
Yes, in the address bar and in copied text | Not by default. Request line and Referer omit it by spec |
| POST field | Not in the address bar | Yes. The request body reaches the server |
| In-memory only, never written into a URL | The other person cannot see it unless you open another channel | No. A one-tap link also cannot finish the handoff |
The split that works: locator in the query, key in the hash
A one-time ciphertext link has to do two jobs at once: tell the server which ciphertext to fetch, and tell the recipient’s browser which key to use locally. Those jobs should not share one HTTP field.
The checkable split is s.html?id={id}#{key}. The query carries only the locator id. The hash carries only the key. On create, the browser runs AES-256-GCM through Web Crypto and is allowed to send ciphertext only. On read, the script reads location.hash, asks the server for ciphertext, and decrypts in this tab. The server is designed to store ciphertext briefly and burn it after reading.
MakePwd Burn-Link follows that boundary. The create page and the reader both open with no account. The reader is public for the recipient and does not ask anyone to sign in. That is still an implementation choice, not “a hash is an encryption protocol.” A fragment itself does not keep a secret confidential. It only keeps the key from leaving the tab as an HTTP field.
Do not test with a real passphrase, an ID number, or an unredacted spreadsheet. Use a disposable sentence and a one-time link you generated for this check. You are comparing a request line with an address bar, not spreading a secret again.
Check it in Network against the address bar
The previous note covered how to use a canary to see whether plaintext left this device as business data. This one narrows the question: did the key enter HTTP? You can finish the steps on their own. You do not have to read that article first.
Create a harmless test string and copy the full generated link. Find the hash: s.html?id=... is in front, the key is after. Open a clean tab, open DevTools, enable Preserve log, then paste the link.
- Compare the address bar with the document request line. The address bar should keep
#and what follows. The document URL should show path and?id=only—not the key after the hash. - Then inspect later XHR / Fetch calls. The ciphertext API may carry
idby design. The same key should not appear again in a body or a query. The create request body should be ciphertext, not the test sentence you just typed. - Open analytics queries and bodies on their own. A page path may appear. If a script writes the full
location.href, the key moves from “default HTTP behavior hid it” to “the page reported it.” That is an implementation bug, not a spec failure.
Three clean passes support a narrow claim only: in this browser, on this open, the key did not leave the tab as an observed HTTP field. Change browser, version, or page script, and you need to run it again.
When this layer of protection fails
A fragment blocks “this HTTP request handed the key to the host.” In the cases below, the key was never relying on default HTTP behavior. The spec cannot help.
First, the full link is pasted into chat, email, or a ticket. The other side sees the whole string with human eyes, and the part after the hash is stored in that system’s history. Some clients drop the hash and preview only what sits before it—then the recipient opens a page that is missing a key, and the reader should say so instead of asking the server for another key. Either way, you already handed a bearer credential to another system.
Second, page scripts and extensions can read location.hash. That is why a reader page works. It is also why XSS or a hostile extension can take the key. RFC 9110 section 17.11 already notes: fragments do not enter the request, but they remain visible to the user agent, to extensions, and to scripts that arrived with the response. If a redirect inherits the original URL’s fragment, it can also carry this site’s fragment onto another origin.
Third, browser history, screen share, and the clipboard. The full address-bar string lands in local history. Cast the tab into a meeting room and the part after the hash is on the wall. None of that goes through your server, and “fragments are not sent over HTTP” does not rebut it.
Fourth, redirect pages or short-link services that rewrite URLs. If the middle page forwards only path and query, the hash is already gone when the recipient opens it. If it first reads the full href in JavaScript and then jumps, the key entered that middle party’s front end. Short links especially need a live check: is the string you hand over still the original one with #?
Three claims that get the story backwards
“The hash is safer, so the whole string can be forwarded freely.” No. Safer toward the server is not safer toward a group-chat log. The full URL is a credential. Whoever has it can open the reader and decrypt.
“Referer might leak the key to an outbound link.” Under current specs, a browser that generates Referer must strip the fragment. What you actually have to watch is the page writing location.href into analytics, logs, or a third-party script. The check is still the Network payload, not the assumption that “any outbound link leaks the key.”
“The reader should require a login, or anyone can open it.” That mixes “who holds the full link” with “who has an account.” Access control for a one-time ciphertext link is the string itself. Putting a sign-in gate on the recipient does not keep the key off the server—the key should never be sent to the server. MakePwd’s reader is public for the recipient. Create and read both need no account.
FAQ
Does the part after the hash get sent to the server?
Not by default. RFC 9110 says the target URI excludes the fragment. In Network, the document request line should not show # or the key after it. The server is designed to see the path and ?id= only.
Why not put the key after the question mark?
A query enters the HTTP request line. The host and the access logs can see it. Once the key is in the query, “ciphertext only” is already false at the log layer. A locator id may sit after ?. The key should sit after #.
Is it still safe to paste the full link into chat?
The server still does not see the key. Chat history, tickets, and browser history do. The full URL is a bearer credential. If you must hand it over, confirm the other person received the original string with the hash, and treat it as something that will live in their history.
Do I need to sign in to open the reader?
No. The reader is public for the recipient: the query id fetches ciphertext, and the hash key decrypts it in this tab. MakePwd has no accounts and no password vault. Create and read both open with no sign-up.
Three things to remember the next time you send a one-time secret
First, watch the character, not the slogan “the key is in the URL.” Queries go over HTTP. Hashes usually do not. Second, on create, confirm outbound data is ciphertext; on read, confirm the request line does not include the part after #. Third, before you hand over the full link, ask whether it will land in chat history, a ticket, or a shortener that drops the hash. Those last two failures have nothing to do with whether the server is zero-knowledge.
If you also need to check whether plaintext left the tab as business data, that is the previous note: search Network bodies and analytics with a canary. This piece only turns “why a key can sit after the hash” into a judgment you can match against the specs and the request line.