cipher.ink Write a message

Learn · Technologies we use

Why the URL fragment (the part after #) never reaches the server

How a secret link keeps its key away from the server that stores the message, shown with a real request, and every place the fragment does leak.

The part of a web address after the # is the fragment, and your browser never sends it to the server. It gets stripped from the address before the request goes out, so it never shows up in the request line, in the server's access log, in any proxy that handles the request, or in the Referer header on the next click. That one fact is what makes a secret link safe to host on a server you wouldn't trust with the secret. The key to a cipher.ink message rides in the fragment, so the server that stores the message has never seen the key. Below we prove it with a real request, then list the places the fragment does go, which matter just as much.

What the standard says

RFC 3986, the document that defines web addresses, says in section 3.5 that the fragment identifier is separated from the rest of the address before it's dereferenced, so that it "is dereferenced solely by the user agent". The user agent is your browser. Fragments were designed to point at a position inside a page (/article#section-2 scrolls to an element), a job only the browser can do once the page has arrived, so the server never needed them. The HTTP specifications agree. The request target in an HTTP request has no place for a fragment (RFC 9110, section 7.1), and the browser leaves it out.

Proving it with a real request

Take a link shaped like the ones cipher.ink makes, with a 43-character key after the # (this key is made up). Ask curl, a client that drops the fragment the way a browser does, to show what it actually sends:

Real output; only the request lines are shown

$ curl -sv 'https://cipher.ink/m/ZZnotthere0001#Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo' -o /dev/null
> GET /m/ZZnotthere0001 HTTP/2
> Host: cipher.ink
> User-Agent: curl/8.13.0

The request is for /m/ZZnotthere0001, and the key appears nowhere in it. The server's own access log records what it received, in the standard Apache format. This is a real line from this site's log, with the visitor's address replaced:

A real access-log line (a test request made while building this page)

IP - - [23/Oct/2026:01:17:08 -0700] "GET /next/m/ZZnotthere0001 HTTP/2.0" 404 7029 "-" "curl/8.13.0"

The log has the message id, the time, the status, the size and the browser, and no key. Anyone who reads those logs, the hosting company included, learns that someone requested a message with that id and nothing more. You can check it yourself in your browser's developer tools: open the Network tab, load a secret link, and look at the request URL, which stops at the id.

How cipher.ink uses it

In secret-link mode the message is encrypted with a key of 32 random bytes generated in your browser, and the link is the message's address plus # plus that key in base64url. The server stores the ciphertext under the address and never learns the key. When the recipient opens the link, their browser requests the page without the fragment, the page's JavaScript reads the key from location.hash (one line does it, linkKey = keyFrom(location.hash)), and the tab decrypts. The read-receipt link, /s/<id>#<token>, works the same way. The token that lets you check or destroy a message lives in the fragment, and goes out only inside a deliberate request to the API. Other tools use the same trick for the same reason: MEGA's file links, PrivateBin pastes, and the discontinued Firefox Send all put the key after the #.

Where a fragment does go

Staying off the server doesn't make it secret. The fragment is part of the link, and a link travels. Here's where it ends up, and what we do about each.

  • Wherever you paste the link. A secret link in a chat or an email is exactly as private as that chat or email, because whoever can read the message can open the link. This is the biggest exposure by far, and it's the reason burn after reading exists: once the first person opens it, nobody else can. The burn article spells out what that does and doesn't guarantee. For high stakes, use passphrase mode and give the passphrase over a different channel.
  • Browser history, bookmarks and sync. The browser stores the full address, fragment included, in its history, and may sync it to the same account's other devices. cipher.ink doesn't remove the key from the address bar after opening, so it stays in history. A message that has burned or expired is useless by then, but one with a long life isn't.
  • Browser extensions with permission to read tabs or history can see the fragment. Anything that can read the page can read the message too, so this comes down to trusting your extensions.
  • Scripts on the page. Any JavaScript running on the page can read location.hash. That's why the site loads nothing from anywhere else, has no analytics, and sends a Content-Security-Policy that stops a stray script from running or phoning home. A third-party script on a page that hosts a fragment key would defeat the whole design.
  • Screenshots, screen sharing and shoulder surfing. The key shows in the address bar and in the sealed card. The card masks the passphrase by default, and nothing can hide a link you copy.
  • The Referer header. Browsers strip the fragment from the Referer they send when you click a link on a page, and cipher.ink also sends Referrer-Policy: no-referrer, so the address of the page you came from isn't passed on at all.
  • Link-scanning services and preview bots. Many email and chat systems fetch a link to check it or build a preview. Most fetch without the fragment and run no JavaScript, so they see only the empty lock card. A scanner that loads the whole link in a real browser could in principle see the key, and for a message that isn't burn-after-reading, whose ciphertext is already in the page, it could decrypt it. For burn messages the ciphertext isn't in the page and Unseal and burn has to be pressed, so a preview can neither use up the message nor read it. See the burn article for details.

What would be a safer design

A fragment key trades one trust for another. The server can't read the message, but the server's JavaScript does the decrypting, and a compromised or coerced server could in principle serve a page that sends the key somewhere. No web page can eliminate that on its own. The article on client-side encryption goes through the options (a browser extension that pins the code, Subresource Integrity, reproducible builds). What we can do, and do, is keep the code small, readable at the annotated source and free of third parties, so there's little to hide a leak in.

Common questions

Can the server owner see the key by any other route?

Not from the request, since it's never sent. The only route is to serve a page whose JavaScript reads the fragment and sends it back, which is the trust issue above. The Content-Security-Policy stops a third party doing that. It can't stop the server itself, and that's why the code is readable.

Does HTTPS hide the fragment from my internet provider?

The fragment isn't sent at all, so there's nothing to hide, and HTTPS encrypts the rest of the request. A network observer sees which site you contacted, and with HTTPS not even the path, only the server name and roughly how much data moved.

What happens if I paste a secret link into a search bar?

Browsers treat what you type in the address bar as a navigation to the page, which is fine. But if the text gets treated as a search query, it may go to your search engine, fragment and all. Paste links into the address bar and check that it loaded the page and not a search results page.

Why not put the key in the query string (after the ?) instead?

Because everything after the ? goes to the server and into its logs, and through every proxy in between. Only the fragment is held back.

Further reading

  1. RFC 3986: Uniform Resource Identifier (URI): Generic Syntax, section 3.5 Fragment
  2. RFC 9110: HTTP Semantics, section 7.1 Determining the Target Resource
  3. W3C Referrer Policy
  4. MDN: Location.hash
  5. PrivateBin: how the zero-knowledge design works