The URL Fragment Exploit: How to Send Data the Server Can't See
If you are building an end-to-end encrypted (E2EE) web application, you face one massive architectural hurdle: Key Exchange. How do you generate a secure link that contains a decryption key, without your own servers seeing that key when the link is clicked?
Putting the key in a standard URL query parameter (like ?key=12345) is a fatal mistake. Query parameters are logged everywhere: server access logs, reverse proxies (like Nginx), analytics software, and ISP traffic monitors. To build a true zero-knowledge app, we must use a native browser feature specified in RFC 3986: The URL Fragment Identifier.
What is a URL Fragment?
The fragment identifier is the part of a URL that follows the hash (#) symbol. Originally, it was designed to let browsers scroll to specific anchor tags on a long HTML page.
But it has a unique, cryptographically beautiful property: Browsers strictly do not send the fragment identifier to the server during an HTTP request.
GET /view?id=8f9a2b&salt=base64xyz HTTP/1.1
Host: zerokey.vercel.app
# Notice that the #SECRET_KEY is completely missing from this network request!
The Vulnerable Way vs. The Zero-Knowledge Way
Let's compare two different shareable links to see why the fragment is critical for privacy.
❌ The Vulnerable Way (Query Params)
https://app.com/secret?id=123&key=MY_DECRYPTION_KEY
When a user clicks this link, the browser asks the server for the path /secret?id=123&key=MY_DECRYPTION_KEY. The server's logs instantly record the decryption key in plaintext. The database admin can now read the user's files.
✅ The Zero-Knowledge Way (Fragments)
https://app.com/secret?id=123#MY_DECRYPTION_KEY
When a user clicks this link, the browser strips the hash. It only asks the server for /secret?id=123. The server fetches the encrypted ciphertext associated with that ID and sends it back to the browser.
Extracting the Key with JavaScript
Once the browser has downloaded the encrypted ciphertext from the database, it can access the hidden key locally using Vanilla JavaScript via the window.location.hash property.
// Extract the hash and remove the '#' symbol
const decryptionKey = window.location.hash.substring(1);
if (!decryptionKey) {
console.error("No decryption key found in the URL fragment!");
} else {
// Proceed to decrypt the ciphertext locally using the Web Crypto API
await decryptPayloadLocally(encryptedData, decryptionKey);
}
Because window.location.hash is evaluated purely in the client's memory, the server remains completely blind to the transaction.
See the Exploit in Production
We used this exact URL fragment architecture to build ZeroKey, an open-source, burn-after-reading file vault. Because the decryption keys are hidden in the hash, even as the database administrators, we mathematically cannot read the payloads passing through our servers.
Summary
If you are building an application that handles highly sensitive data—passwords, API keys, or personal messages—you must adopt a zero-knowledge mindset. The server should be treated as a hostile environment. By shifting the key exchange to the URL fragment identifier, you leverage a fundamental rule of HTTP to guarantee absolute privacy for your users.