Why a URL hash can hold a key, and when that protection fails

A question mark and a hash look like two punctuation marks in the same address bar. The query after ? is written into the HTTP request line, so the host, reverse proxy, and access log can see it. The fragment after # is left for this tab; by default it is not sent with that document request. Putting a decryption key after # blocks “the host saw the key.” It does not block “someone forwarded the whole link.”

Start here: a hash is a fit place for a key because the browser drops the fragment when it builds an HTTP request—not because a new crypto protocol appeared. The check is to compare the address bar with the Network request line. Once you paste the full URL into chat, a ticket, or browser history, that layer no longer holds against anyone who can see the string.

Answer the search intent in one sentence

A URL hash can hold a key because RFC 9110 says the request’s target URI excludes the fragment. When the browser asks the server for the document, it strips # and everything after it. The host, CDN, and access log are designed to see the path and the query after ?—not the key after the hash.

This note is for developers, ops, and anyone who wants to separate “the server cannot see it” from “chat history cannot see it” before they send a one-time link to a colleague. It is not a substitute for the Burn-Link how-to, and it does not repeat how to create a link. What you should take away is which layer ? and # each enter, how to compare them in Network, and which protection dies when the whole URL is forwarded.

The query enters HTTP; the hash stays in the tab

One address can carry both a query and a fragment. The shape is https://example.com/s.html?id=abc#key. After the question mark, id=abc is the query. After the hash, key is the fragment. When the browser parses the string, the fragment runs from the first # to the end. A literal hash earlier in the URL must be written as %23, or parsing cuts off too soon.

For the document request, the browser assembles Host, path, and query. MDN’s note on the URI fragment is blunt: the fragment is not sent to the server when the resource is requested; the client handles it after the document arrives. In HTML it often scrolls to an id. On a burn-after-read page, the same slot is borrowed to carry the key this tab needs for local decrypt.

So “the server stores ciphertext only” holds only if the lookup id travels in the query, the decryption key travels in the fragment, and page scripts never write location.hash into a later request. Miss any of those, and the hash is decoration.

The specs describe default behavior, not extra encryption

RFC 3986 §3.5 defines the fragment as an identifier for a secondary resource on top of the primary one, interpreted by the client according to the document type. RFC 9110 §7.1 then states that the target URI excludes any fragment in the reference, because fragment identifiers are reserved for client-side processing. Read together: the part after # is not part of the HTTP request target.

That is why access logs have no #. Ops did not hide a field. The request line never contained that span. Trying to count “which heading a user clicked” from server logs will fail for the same reason. Trying to show “the key never reached the server” from the same logs lines up with the spec.

Referer follows the same rule. MDN’s Referer page says the header may carry origin, path, and query, but must not include a URL fragment or username and password. Referrer Policy empties the fragment before a URL is packed into Referer. So when the read page later fetches an image or a script, the key after # is not handed to a third-party host by default. That strip is browser default behavior—not a protocol this site invented.

Page scripts can still read the fragment. MDN’s URL.hash is # plus the fragment itself; the current page can also use location.hash. The read page uses that interface to take the key, then calls Web Crypto for AES-256-GCM decrypt. The same interface can be misused: a script can append the hash to a query, or send it in an analytics payload. The spec blocks the request line the browser builds by default. It does not block “the page read the hash and sent it anyway.”

Do not experiment with a real password, API key, or an unredacted connection string. Prepare a one-time test sentence. You are checking request lines and bodies—not exposing a credential again.

Compare the address bar with the request line in Network

A slogan is not evidence. A check you can finish now is to open the Network panel and put the full address-bar string next to every request URL. The previous note used a canary to search for plaintext. This one watches only the fragment: the span after # should appear in the address bar and should not appear on the request line.

  1. Open the read page and confirm the address bar looks like s.html?id={id}#{key}: the query holds only the id, and the key sits only after the hash.
  2. Open Network, check Preserve log, and start the filter on All. Do not hide the document request, later APIs, or analytics yet.
  3. Paste the key after # into the filter as a canary. A red hit means stop and read that request: the implementation put the fragment into a query, or a script read it and sent it.
  4. If there is no hit, open the document request and interfaces such as /secrets one by one. The request line should carry only id; the body should not contain the original text.

You can run the same check on create. After this device encrypts with AES-256-GCM, the outbound fields should be ciphertext, expiry, and read count. The random key should appear only in the share link you assemble next—not in the POST JSON. FastPwd’s Burn-Link follows that boundary: create and read are both ready to use, with no account. A promise is still only a promise. Trust what this Network session shows.

Keep %23 and a real # apart. Characters after %23 on the request line were sent as path or query, so the server can see them. Only an unencoded # starts a fragment. Writing the key as ?key= or /%23key can look like “it is hidden in the hash.” To a log, it is already a plaintext query.

Page scripts and analytics can still read the hash

A default HTTP request without a fragment is not the same as “no script in this tab can see it.” JavaScript on the current document has location.hash. The read page must read it to decrypt locally. If an analytics script reports the whole location.href, the key shows up in that tracker’s query or body.

So a canary search cannot stop at the business API. Filter the analytics path on its own and open query and body. Page title and path may appear. The key after #, a password you are auditing, and text before redaction should not. FastPwd’s visit analytics go through the site’s own tracker and do not send from localhost by default. Even then, use the same canary to confirm the report has no fragment key.

Extensions, enterprise DLP, and some “AI browsers” can also read the full address bar. They do not travel on the HTTP request line you are watching, so the Network panel will not prove they stayed out. Shrink the claim to a sentence a colleague can reuse: “I searched the request line and the analytics payload; the key after # is not there.” Do not upgrade that to “no software on this computer ever saw this hash.”

When this layer of protection fails

The fragment blocks “this document request handed the key to the host.” In the cases below, anyone who sees the full URL already holds the key. The spec cannot help.

First: pasting the full link into chat, email, or a ticket. The message body contains the part after #. The other client, the room history, ticket search, and a later export can all see the string. The server may still never receive the key over HTTP. Anyone who can open the thread already has it. The full URL is a bearer credential.

Second: browser history and sync. A # fragment that appeared in the address bar usually lands in local history. A browser with sync on may copy the same record to your other devices. Closing the tab does not clear history.

Third: a redirect can carry the fragment along. If the Location target has no fragment of its own, many browsers append the original fragment to the new address. The key then sits in an address bar you thought had already “left the sensitive page.” If the read page is redirected to an analytics host or a login host, read the final request line in Network first, then see what is left in the address bar.

Fourth: link previews and security scanners fetch the part before the hash. When a chat app builds a preview, it should not send the fragment to the host being previewed, but it will request s.html?id=…. If the implementation burns on the first GET of the document or the first GET of the ciphertext, the preview bot can spend the read count before the recipient clicks. The recipient then sees “already burned.” The key was never given to the preview host—the read count burned, not the fragment’s secrecy.

FastPwd’s read page splits those two steps: opening the page only asks whether the ciphertext is still there; it does not take the ciphertext. After the recipient clicks to open and view, the page fetches ciphertext by id and decrypts locally with the key after #. Neither the create page nor the read page needs an account. Preview bots usually fetch the landing page and do not click the button for you, which lowers the chance of “preview equals burn.” It does not guarantee every scanner stops at the landing page. The count is still a server-side counter, not part of the fragment.

Who is looking Can they see the key after # Does this layer still hold
Document request line Not by default Yes; compare in Network
Host access log / CDN Not by default Yes, if the key never entered the query
Referer to a third party Not by default Yes; the spec strips the fragment
Page script / analytics Can read location.hash Depends on the implementation; search the body with a canary
Chat, email, ticket body Sees the whole string Does not hold against the person you forwarded it to
Browser history / sync Usually yes Does not hold against anyone who can open this browser
Link-preview bots Should not receive the fragment May consume a read count

What you can prove, and what you cannot

This check supports a narrow claim. It supports: in this browser, on this open, the document request and the interface request lines you observed did not carry the key after #, and the create request body did not contain the original text.

It does not support: a chat admin never saved the full link; an extension never read the address bar; the recipient will not take a screenshot; ciphertext will never reappear in another backup after it is fetched. Burn-after-read deletes ciphertext on the server. It does not delete plaintext already decrypted onto the screen, and it does not delete the full URL sitting in a chat thread.

Do not read a fragment as “only the intended recipient can open this.” Anyone who holds the whole ?id=…#key string can decrypt on the public read page. The read page has no login gate. That is the design: the other person does not have to register first. The cost is that the whole string must be stored like a credential, not like a document link you can forward in a group.

FAQ

Does the part after # in a URL get sent to the server?

Not by default. RFC 9110 says the target URI excludes the fragment. When the browser requests the document, it strips the hash and everything after it. The host, reverse proxy, and access log are designed to see only the path and the query after ?. If the key after # appears on the request line, that is an implementation bug, not a spec exception.

Why not put the decryption key in the query string?

The query after ? is written into the HTTP request line. The host, CDN, and access logs can all see it. Once the key sits in the query, “the server stores ciphertext only” is already false at the log layer. An id may live in the query, because it is how you fetch ciphertext. A key may not.

Is it still safe to paste a full hash link into chat?

The server still cannot see the key in that HTTP request. Chat history, tickets, and browser history can. The full URL is a bearer credential: anyone who has the whole string can decrypt on the read page. If you need more isolation, send the link that carries the id and the key after # on two channels—do not drop the whole string into the same room.

Do I need an account to open a Burn-Link read page?

No. The read page is public for the recipient. It fetches ciphertext with the id in the query and decrypts locally with the key after #. FastPwd has no accounts and no vault. Create and read are both ready to use. A login gate would not fix the fragment boundary, and it should not be written into this check.

Verify it on a disposable test link

If you want a page that already splits query and fragment, start with FastPwd’s Burn-Link. It is ready to use—no account. Write a throwaway test sentence and generate a locally encrypted link. In the create request you should see ciphertext plus count and expiry fields. The share link is the one that looks like s.html?id={id}#{key}.

Open the read page with that string. The address bar should show both ? and #. In Network, the document request and the status check should carry only id. After you click to open and view, the request that fetches ciphertext should still omit the key after #. Plaintext should appear only on the page after local decrypt. The algorithm is AES-256-GCM; the work runs in Web Crypto. A single text payload is capped at 32 KB. For a whole file, use the File Encryption Box: one file up to 5 GB, output .lock / .enc.

The point of the exercise is not to prove any one site is “absolutely safe.” It is to keep two facts apart. Layer one: the host cannot see the fragment, by spec. Layer two: anyone who sees the full link holds the key. On any page that claims “the key sits after the hash,” the steps stay the same: compare the address bar with the request line, inspect analytics on their own, then ask whether this URL will land in a chat thread.

Before you send the next one, keep three lines. First: an id may enter the query; a key may enter only the hash. Second: a clean request line only means the server did not receive the key. Third: store the full link like a credential—do not forward it as an ordinary document URL. An observation you can repeat is the one worth writing into your own handoff notes.

Further reading

The previous note turns “did plaintext leave this tab?” into a repeatable Network check. Read it next to this one: How to check if browser encryption never uploaded plaintext. This article only splits ? from #, and names the layer that dies when you forward the full link.