Home Generator Audit Privacy Burn link File box

Why the decryption key belongs after # in the URL

The easy mistake in one-time secret sharing is not a wrong algorithm name. It is putting the key in a query parameter. By the URI spec, a browser does not send the fragment after # to the server; access logs usually keep only the path. Below: who can see which part, and steps you can check on the spot.

When an operator sends a database password to a colleague, the cheap version is a link they can click. The trouble starts the moment that link is opened: it passes through the browser, a reverse proxy, the origin, and a pile of logs. What you actually control is not “does the encryption sound safe.” It is which layer the decryption key appears on.

If the key is written after ?key=, it becomes part of the HTTP request and almost always lands in the access log. If it is written after #, the browser strips it before the request is sent — not because a product promised that, but because that is how URI fragments work. This piece answers one question: why the key must sit after the hash, and how you can see that the server never received that segment.

What a query-parameter key leaves in the logs

Query parameters (after ? and before #) are part of the request target. When the browser requests /s.html?id=abc&key=MYSECRET, the full path plus query goes on the wire. The origin process sees it. So do Nginx, the CDN, the WAF, and APM in front. Default access-log formats include the request URI, so key=MYSECRET is written on the same line as the status code and timing.

That line is hard to erase later. Logs rotate to disk, then into backups, SIEM, and the “copy this 500” a support agent pastes. Error trackers like to sample request bodies and URLs; one failed decrypt can drop a keyed address into a crash report. Jurisdiction and subcontracted ops turn “we do not read logs” into a sentence you cannot audit. You can check the protocol, not the other party’s internal runbook.

There are quieter copies too: the browser’s own history, a screenshot of the “full URL,” and monitoring scripts that report the entire location.href. Query parameters at least land a copy on the server first. A fragment blocks that copy, not every copy on the device. Blocking the server side is the premise that makes one-time sharing possible.

Putting the key in a POST form does not fix it. If a create API writes plaintext or the key into JSON, the server can still read it. The right constraint: the only things that leave the browser are a ciphertext id and the ciphertext itself. Material that can open the ciphertext must not appear in any field that will be logged.

HTTPS does not stop the other end from reading query parameters

TLS protects eavesdroppers on the path. After the origin terminates TLS, it has the decrypted request line. Query parameters are plaintext to the server. A fragment never appears in that request at all.

Where the part after # actually goes in HTTP

A URI splits into scheme, host, path, query, and fragment. The fragment starts with # and was first meant for in-page anchors. The spec requires the user agent to strip the fragment before requesting the document. The HTTP request line has no #... field, so proxies and the origin cannot write it into the access log.

After the page loads, a script can read location.hash. That is a string in client memory, not a field the server returned. A typical reading page: fetch ciphertext with an id in the path or query, then decrypt with AES-GCM using the key read locally. The server only ever handles ciphertext. Without the key, that ciphertext is useless to it.

MyPassGen’s zero-knowledge burn link follows that order: encrypt in the browser with AES-256-GCM, encode a random 256-bit key as Base64URL after #, and upload ciphertext plus metadata such as expiry and read count. The reading page is public to the recipient. They do not register first — otherwise one-time sharing would require opening an account for a colleague, and the chain would already have failed. After the first successful read, ciphertext is burned by design. Those are product facts, not “the server promises not to look.”

What you should see in the request, and what you should not

The create POST body should contain ciphertext, not the original you just typed, and not the key that later appears after #. The read GET should carry only the ciphertext id; the Request URL in developer tools should stop before #. If searching a substring of the key hits a request, the implementation put the key in the wrong place, or an analytics script sent the full location.href.

URL part Example Can the server / proxy see it? Fit for a key?
Path /s.html or /s/{id} Yes; lands in the access log Ciphertext id only, never the key
Query ?id=abc&key=… The whole string; logged by default No; key= is a log line
Fragment Base64URL key after # Not present in the HTTP request Yes, as a browser-only key channel
Request body JSON on create The origin sees it; some proxies sample it Ciphertext and non-sensitive metadata only

“Zero knowledge” here is narrow: the server stores data it cannot open. It does not mean “store nothing,” and it does not mean “someone with the full link cannot decrypt.” The full link is a decrypt-if-you-hold-it credential. Without the part after # it will not open. Copying the full link is copying another key.

Confirm with the Network panel that the key did not go on the wire

Slogans cannot prove themselves. The steps below use only the browser’s developer tools. They unmask both “the link has a key, so the server must have seen it” and the opposite, “the page says local encryption, so nothing left.” The goal is not a formal proof. It is whether this click put the key into HTTP.

01

Prepare a test link that includes a hash. Use a burn link you just created, or any local page plus #TESTKEY-for-checking-only. Copy everything after # into a notes file. You will search for it later.

02

Open Network and enable Preserve log. Chrome, Edge, and Firefox all work. Look at Fetch/XHR and document requests first, then scan everything. Preserve log keeps the list across navigations. The address bar still shows the full URL. That is the browser’s own display, not what went on the wire.

03

Open the link or press Enter. In the request list, open the document request and later XHR. Look at Request URL: it should stop before #. The query may contain a ciphertext id. It should not contain the key you saved in the notes file.

04

Search the whole panel for a key substring. Paste the key, or a substring that will not appear in static scripts, into Network search. If no request hits, at least this navigation did not send the fragment as HTTP content.

05

Contrast with the wrong spelling. Move the same key to ?key= and visit again (use a value that cannot cause a real leak). This time the search should hit the request URL. The two results side by side are clearer than any diagram.

06

Search the original again on create. If you submitted a throwaway test password on the create page, search that original in the POST body. You should hit a ciphertext-shaped field, not the original, and no key field. The reading page is public to the recipient. They do not log in first.

What this proves, and what it does not

Network checks whether this action put the key or the original into HTTP. It cannot see a malicious extension, a script injected by XSS, or the page after a corporate SSL-intercept proxy. That is enough to unmask “the key is in the query.” It is not an endpoint-security proof.

A fragment blocks logs. It does not block forwarding the full link

Putting the key after # solves “the origin and proxies can read the key by default.” The paths below still exist. Writing them into the expectation is more useful than printing three algorithm letters.

The full link decrypts if you hold it

Anyone with path plus fragment can open the ciphertext on the reading page — until it is burned or expired. Chat history, mail archives, browser history, screen sharing, and shoulder surfing are outside the HTTP spec. Burn-after-read reduces repeat opens and long-lived server plaintext. It cannot stop a screenshot or a forward.

Referer, preview cards, analytics scripts

Modern browsers usually omit the fragment when they send Referer. That rule does not save an implementation that writes location.href into an analytics event. If the create or read page reports the full address as a tracking property, the key leaked through a client channel even if the server access log is clean. Search the same key again when you inspect those tracking requests.

Chat and mail clients do not always keep the part after # in link previews. Some fetchers request only the path — the preview fails, but the key is still in the text you pasted. Some rewrite a short link and drop the fragment, so the other person sees “incomplete link.” Open it yourself in a private window before you send. Confirm the reading page decrypts. That is more reliable than trusting a client to “open as pasted.”

Browser history and local scripts

The full URL in the address bar goes into history. A shared computer or a synced history profile leaves a key that still works until expiry. XSS or a hijacked script can read location.hash: Web Crypto protects key operations on an honest page, not a malicious script that already shares the origin. Those are endpoint and frontend supply-chain problems. Keep them separate from “the server should not hold the key.”

What must stay after #, and what may go to the server

What must stay on this device, and may only be handed to the recipient’s browser through the fragment, is material that restores plaintext: the symmetric key itself. Once it enters a query or a request body, the zero-knowledge story is broken.

What may leave the device is data that is useless to the server: the ciphertext blob, an unguessable id, expiry, and a maximum read count. MyPassGen burn links cap each note at 32 KB — passwords, API-key fragments, and short notes, not a whole disk image. Large files should become .lock / .enc on this device first, then travel another way. The server deletes ciphertext after the first successful read by design; unread notes that expire are deleted too. There is no server-side plaintext copy. A lost link cannot be “restored by support.”

Do not confuse “the page requested a script” with “the key was uploaded.” Static-asset requests are normal. What you look for: did a key substring appear in the request URL, Query, Payload, or a custom header? This site also does not report keys or originals as analytics event content. Events should not contain the secret itself.

When you want to send a key to a colleague without writing it into server logs

Common alternatives have specific limits. Plain email and chat leave the password in both histories, server archives, and device backups. Expiry does not delete the other person’s copy. Drives are readable to the server by default unless you encrypt on this device first. A key in a query parameter is an invitation for the reverse proxy to write it down. Those paths are not “less convenient.” The plaintext window is written into the protocol.

If you are sending a short secret — a root password, a recovery code, a short connection string — you need: encrypt here, keep the key out of HTTP, let the recipient open it without an account, and destroy the ciphertext after the read. MyPassGen’s burn-after-read link is built on that constraint: AES-256-GCM in the browser, the key after #, ciphertext only on the server; create and read without registration. Open Network, search the key you just copied, and confirm the segment that should not appear in logs never went on the wire.

Over 32 KB, or a backup you must decrypt more than once, belongs in the file encryption box: make .lock / .enc here, then pick your own transport. A burn link is for a short secret that should be seen once, not a drive replacement. For why sensitive work should not default to a server, the previous piece Why encryption should run in the browser opens the plaintext window further. This piece only asks you to answer: when this link is opened, is the key still after the hash, or did it run into the request line?