Browser requests leave more copies behind than most of us realise. A request can pass through browser history, a cache, a service worker, an extension, a CDN, a load balancer, a reverse proxy, a framework, tracing, exception reporting and several log stores before the handler does anything useful with it.
TLS is essential, but it protects the request only while it travels between TLS endpoints. Once an endpoint decodes it, the request can be copied, indexed or logged like any other data. URLs turn up in histories and access logs. Credential headers turn up in request dumps. Custom headers evade redaction. Cookies are stored on purpose. Fragments are visible to page code. Bodies are often captured when something goes wrong.
So the useful question is not where can I put a secret so that it cannot leak? It is which part of the request is least likely to leak for this threat model, and what else do I need to do?
For the short version, go directly to the list of request carriers and their leakage pitfalls.
This guide is about accidental persistence and disclosure. A fully compromised browser or origin is a different problem: malicious code running with the application’s authority can usually read the same data or make the application use its credentials.
Treat every part of an HTTP request as potentially observable. Some parts are still much less accident-prone than others.
There are two useful ways to improve a design: reduce the number of places that can copy a value, and reduce what a copied value is worth. The figure shows both.

The OWASP Top 10:2025 covers this problem, but in pieces:
Earlier editions named the outcome more directly: A3:2017 Sensitive Data Exposure. The newer structure is organised around causes. That makes sense for classification, but it is less helpful when the immediate question is whether a value belongs in a header, cookie, body, fragment or URL.
The supporting cheat sheets give sound advice: keep credentials out of URLs, exclude sensitive values from logs, and make session identifiers opaque. What they do not give us is one comparison of all the places a request value can be copied: URLs, standard and custom headers, cookies, fragments, bodies, caches, errors, tracing and observability systems.
That narrower comparison is what this guide is for. It complements the OWASP material rather than replacing it.
These ratings describe common defaults, not protocol guarantees. Configuration can make any row worse. A linked seal, đź¦, takes you to the longer discussion.
| Carrier | Browser persistence or history | Browser Performance Timeline | Ordinary access logs | Error, debug, and trace capture | Same-origin JavaScript | Potential Exposure | Appropriate use |
|---|---|---|---|---|---|---|---|
Authorization: Bearer … 🦠|
Low for application-supplied bearer tokens; browser-managed HTTP authentication may be retained separately | Low: the API exposes URLs, not header values | Usually low because many systems recognize it as sensitive | Medium: redaction is common but not universal | High when the application owns the token | First Party Scripts Third Party Scripts Browser Cache/Storage/History CDN/Proxy/TLS termination Tracing/Debugging/Extensions Logging at Origin Downstream Services |
Short-lived, scoped API credentials |
Cookie with Secure; HttpOnly; SameSite 🦠|
High by design: the browser stores it | Low: the API does not expose cookie values | Usually low, but highly configuration-dependent | Medium: full request dumps may include it | Low for directly reading an HttpOnly value; high for causing authenticated requests |
Browser Cache/Storage/History CDN/Proxy/TLS termination Tracing/Debugging/Extensions Logging at Origin Downstream Services |
Opaque browser session identifiers |
Proxy-Authorization |
Low | Low: the API does not expose the header value | Usually low at the origin; visible to the authenticating proxy | Medium | Usually low | CDN/Proxy/TLS termination Tracing/Debugging/Extensions |
Proxy authentication only; never repurpose it |
Custom secret header such as X-API-Key 🦠|
Low unless application code persists the value | Low: the API exposes URLs, not header values | Usually low in traditional access-log formats | Medium–high because generic redactors may not recognize it | High | First Party Scripts Third Party Scripts Browser Cache/Storage/History CDN/Proxy/TLS termination Tracing/Debugging/Extensions Logging at Origin Downstream Services |
Machine or API credentials when Authorization cannot be used |
| Request body 🦠| Usually low in browser history and HTTP caches | Low: the API does not expose the body | Usually low in traditional access logs | High in practice, especially on errors | High | First Party Scripts Third Party Scripts CDN/Proxy/TLS termination Tracing/Debugging/Extensions Logging at Origin Downstream Services |
Necessary PII or structured sensitive input, with minimization and redaction |
| URL fragment 🦠| High until removed; may enter history and history sync | High when retained in a navigation or resource entry; browser-dependent | Low, but not zero in operational reality | Medium in browser telemetry and page-level error reports | Very high | First Party Scripts Third Party Scripts Browser Cache/Storage/History Tracing/Debugging/Extensions Downstream Services |
Narrow, isolated bootstrap and split-knowledge designs only |
| URL path or query string 🦠| Very high | Very high: navigation and resource entries expose request URLs | Very high | Very high | Very high | First Party Scripts Third Party Scripts Browser Cache/Storage/History CDN/Proxy/TLS termination Tracing/Debugging/Extensions Logging at Origin Downstream Services |
Non-sensitive routing and filtering values only |
Referer, User-Agent, Forwarded, or X-Forwarded-For |
Medium | Low: these request headers are not exposed | Very high | High | Varies | First Party Scripts Third Party Scripts Browser Cache/Storage/History CDN/Proxy/TLS termination Tracing/Debugging/Extensions Logging at Origin Downstream Services |
Protocol metadata only; never repurpose for secrets |
Tracing baggage or similar context 🦠|
Usually low | Low unless copied into a URL | High across downstream telemetry | Very high by design | High when created in the browser | First Party Scripts Third Party Scripts CDN/Proxy/TLS termination Tracing/Debugging/Extensions Logging at Origin Downstream Services |
Non-sensitive correlation metadata only |
“Low” does not mean “safe.” It means that the value is less likely to appear in that particular sink under conventional defaults.
RFC 9111 defines an HTTP cache key as, at minimum, the request method and target URI. In practice, most caches focus on GET responses and use the URI as the main key. A response can add request headers to cache matching through Vary.
Sensitive headers should therefore never be named in Vary:
Vary: X-API-Key # unsafe design
The cache now has to retain enough information to distinguish the original header values. Authorization does not need to appear in Vary; authenticated requests already have special shared-cache rules.
For a sensitive exchange, both requests and responses can use:
Cache-Control: no-store
For Fetch, the corresponding client instruction is:
await fetch(url, {
cache: "no-store",
headers: { Authorization: `Bearer ${token}` },
});
no-store tells compliant private and shared caches not to store this request or response. RFC 9111 explicitly warns that it is not a privacy mechanism. It says nothing about application logs, exception reports, traces, packet capture, extensions or a compromised intermediary. private is weaker again: it still permits storage in a browser cache.
HTTP/2 HPACK and HTTP/3 QPACK compression tables are separate from response caches. HPACK has a “never indexed” representation for sensitive fields and names Cookie and Authorization as examples, but the implementation has to use it. Treat this as defence in depth, not a security boundary.
Traditional access logs usually record the request line—which contains the URL—plus the referrer and user agent. The standard nginx combined log format, for example, does not include arbitrary request headers or the body. That gives Authorization, cookies, custom headers and bodies lower default access-log exposure than URLs.
The advantage often disappears elsewhere. Exception middleware, debug logging, API gateways, request inspectors and error-reporting tools have a habit of serialising the complete request when something fails. Some systems do it for ordinary structured logs too. A common half-fix is to redact Authorization while keeping cookies, unfamiliar headers, parsed form fields and the whole body.
The OWASP Logging Cheat Sheet says to remove, mask, hash, sanitise or encrypt access tokens, session identifiers, keys and sensitive personal data instead of recording them directly. Good advice, but not a safe assumption about a system you have not inspected. Treat bodies and unrecognised headers as loggable.
Observability has the same problem. The OpenTelemetry HTTP semantic conventions recommend explicitly choosing which headers to capture because “capture everything” can disclose sensitive data.
Even well-redacted logs are security-sensitive. Restrict and audit access, protect their integrity, and set real retention and deletion periods. “It might be useful later” is not enough reason to keep personal data indefinitely; harmless-looking identifiers can become identifying when they are combined.
Authorization is usually the least accident-prone header for a bearer credential because the surrounding ecosystem knows what it means:
Authorization header for OAuth bearer tokens and warns against putting bearer tokens in page URLs.Those conventions help, but they are not confidentiality guarantees. Every TLS endpoint and application layer that sees the decoded request can still read the header. So can a full request dump, a badly configured trace exporter or a custom proxy.
Bearer credentials should be short-lived, audience-restricted, narrowly scoped, and replaceable. A cryptographic private key should not be transmitted in Authorization or anywhere else in an HTTP request.
For a browser session, an opaque cookie with Secure, HttpOnly and an appropriate SameSite setting is usually better than giving JavaScript an API token. HttpOnly stops ordinary page scripts reading the cookie; it does not stop them making authenticated requests through the browser.
Cookies are stored by design and sent automatically to matching destinations. Expect to find them in browser profiles, backups, developer tools, endpoint inspection products, privileged extensions, proxies and server-side request dumps. Keep the value opaque; raw PII does not belong in it. The __Host- prefix can narrow a session cookie’s scope further.
For browser OAuth clients, RFC 10017 presents a backend-for-frontend (BFF) as the strongest of its principal architecture patterns: OAuth tokens remain at the backend and the browser holds only a cookie-backed session. Malicious page code can still act through the user’s browser, but it cannot directly extract the backend tokens.
A custom header such as X-API-Key avoids browser history, referrer propagation and conventional request-line logging. It may still be worse than Authorization: generic infrastructure has no reason to recognise your header as sensitive.
Every custom credential header should be registered with redaction policy at all of the following layers:
A custom secret header must not be included in Vary. Cross-origin use also causes a CORS preflight in ordinary Fetch configurations; the preflight exposes the header name, not its value.
A request body avoids the browser-history, referrer and conventional access-log problems of a URL. HTTP caches mainly store responses, and a POST response is not normally keyed by the request body.
In practice, bodies are logged surprisingly often. Frameworks parse them into convenient objects, then exception handlers and structured loggers serialise those objects. Validation failures are a particular trap: the bad input is attached to the error because it is useful for debugging. A proxy or APM agent may also capture the raw body before application redaction runs.
Necessary PII belongs in a body rather than in a URL or arbitrary header, but it should still be minimized. Schemas should classify sensitive fields, and log serializers should operate on an allowlisted safe view rather than serializing the parsed request and deleting a few known keys afterward.
Application-layer encryption can keep selected body fields opaque to CDNs, TLS-terminating gateways, and early request logging. It does not help after decryption unless the resulting value retains its sensitive classification throughout the application.
Under the URI, HTTP, and Referrer Policy specifications, the fragment is interpreted by the client. A conforming browser does not include it in the HTTP request target or in the Referer header.
The specification is clear; real systems are messier. Fragment values sometimes reach servers through embedded or nonconforming user agents, extensions, native wrappers, analytics, error telemetry, or client code that copies location.href or location.hash into another request. Treat a fragment as unlikely to reach ordinary server infrastructure, not as cryptographically prevented from doing so.
Every script on the page can read the fragment through window.location. It can also appear in browser history, synced history, copied URLs, screenshots, crash reports, session replay and extension APIs. OAuth’s former Implicit flow returned access tokens in fragments; RFC 10017 now prohibits that flow, in part because third-party or malicious page code can read the token.
When a fragment is used for a narrowly scoped bootstrap value, it should be removed before third-party code loads:
<script>
const bootstrapValue = location.hash.slice(1);
history.replaceState(null, "", location.pathname + location.search);
</script>
This should be the first executable code on a small, isolated landing page. Do not load third-party scripts or redirect first. Use a strict Content Security Policy and discard the value as soon as possible. replaceState reduces later exposure; it cannot undo an earlier capture.
Removing a fragment from window.location and history does not necessarily remove it from the Performance Timeline. Navigation Timing creates an entry from the document URL, while Resource Timing stores requested resource URLs. The history update steps used by history.replaceState do not rewrite entries that already exist.
Same-origin code that runs later may therefore be able to recover an initial navigation URL or the URLs of earlier Fetch, XHR, image, script, and other resource requests:
const initialURL = performance.getEntriesByType("navigation")[0]?.name;
const resourceURLs = performance
.getEntriesByType("resource")
.map(entry => entry.name);
The Performance APIs expose URLs and timing, not headers or bodies. They will not reveal Authorization, a cookie or a body field unless somebody also put that value in a URL. Fragment handling varies between browsers, especially for special fragment directives, so assume an ordinary fragment may remain in a navigation or resource entry after visible cleanup.
performance.clearResourceTimings() removes buffered resource entries, but there is no equivalent for the current navigation entry. Nor can clearing retract a value already copied by a PerformanceObserver, analytics library, tracing client, extension or earlier script.
If later code must not recover the bootstrap URL, run the bootstrap in an isolated document and then load a genuinely new document whose initial URL never contained the fragment. Do that before introducing analytics, logging, tracing or other nonessential code. Replacing the fragment in the same document is not enough.
This is why I would not describe a fragment as a leak-proof home for PII or reusable credentials. At most, use one for a narrow, short-lived, single-use bootstrap value or decryption key, and include its possible disclosure in the threat model.
In a split-knowledge design, the server-visible location holds ciphertext and the fragment holds its key. The server, CDN and conventional request logs see ciphertext; the page decrypts it locally.
A framework implementing this pattern should provide:
This keeps plaintext out of server-side caches and early logs. It does nothing about history capture before cleanup, malicious same-origin JavaScript, extensions, a compromised browser profile or code that sees the decrypted value. If the original URL contains enough information to recover both ciphertext and key, anyone with that URL can decrypt the PII.
Paths and query strings have the widest accidental-disclosure surface. They turn up in:
Referer headers and, under weaker policies, cross-origin referrers;The default referrer policy, strict-origin-when-cross-origin, still sends the full path and query on same-origin requests. Referrer-Policy: no-referrer limits that propagation, but none of the other copies disappear.
URLs should contain opaque, short-lived, single-use handles rather than PII or bearer credentials. RFC 9700 discusses even short-lived OAuth authorization codes as a browser-history exposure and relies on replay prevention and prompt cleanup.
Tracing is designed to copy context across process boundaries. That makes its metadata a particularly bad place for sensitive data.
OpenTelemetry baggage may be forwarded automatically to downstream services and copied again into spans, metrics and logs. Its security guidance warns that baggage can reach unintended resources, including third-party APIs. Keep credentials, user identifiers, email addresses, health information and other PII out of baggage, tracestate, correlation headers and metric labels.
These terms are often used as though they were interchangeable. They are not.
All of these techniques either make a leak less useful or reduce where plaintext appears. None makes leakage impossible.
A useful one-time handle is random, short-lived and deliberately boring. It has one job:
Do not confuse “single-use” with “one HTTP delivery attempt”. Browsers, proxies and applications retry, and a successful response can be lost. An identical retry should recover the first result without repeating the side effect; a changed payload or action should fail. This gets awkward across replicas, concurrent tabs, partial failures and expiry, which is why it belongs in shared infrastructure rather than every handler.
Store a hash instead of the redeemable handle when a hash is enough. And do not put the handle in a URL merely because it is opaque. Before redemption it may still be a credential, with all the usual history, logging, referrer and telemetry problems.
Tokenisation cannot replace data the server has never received. If somebody enters PII in a browser, there is still an initial plaintext collection step. Keep that path as narrow as possible:
The submission handle limits who can submit, what they can do and whether the action can be replayed. It does not hide data already sitting in form controls or JavaScript memory. First-party code, third-party code running with first-party authority, extensions and a compromised page can all see the PII before encryption. WebCrypto does not change this; its security considerations warn that injected script can expose keys or data and that an application can read the messages it processes.
For especially sensitive collection, use a small isolated page without analytics, session replay, advertising, tag managers or other nonessential code. A strict Content Security Policy and field encryption help, but neither protects plaintext from malicious code that already has the page’s authority.
Sooner or later the PII has to be evaluated, displayed, delivered or otherwise used. If not, there was little reason to collect it. Tokenisation does not remove this revelation boundary; it reduces how many systems cross it.
The resolver should return only what the caller needs. Often that is “age requirement satisfied”, “account verified” or “delivery permitted”, not the whole record. This is ordinary data minimisation; it does not require a zero-knowledge proof. Zero-knowledge techniques help when a verifier can act on a fact alone. They do not help when the source data is genuinely needed for delivery, treatment, investigation or support.
Private keys are a useful exception to “the data must eventually be revealed”. A key may need to sign or decrypt without ever leaving an HSM, KMS or tightly scoped service.
This leaves fewer plaintext leak points, but each one matters more. The ingestion service, resolver, PII store, authorised consumers and key-use service become the crown jewels. Give them strict access control, purpose limits, safe request representations, schema-derived redaction, short retention and leak-canary tests. Remember that an opaque handle may itself be personal data if it can be linked back to a person.
This buys us fewer meaningful copies and easier expiry, replay detection and revocation. The cost is server-side state, a resolution dependency, awkward retries, recovery friction, harder delegation and a high-value vault. Authority and plaintext have been contained, not abolished.
Picking a better request carrier helps, but most leaks happen across the whole path. These protections are difficult to implement correctly in every handler, so they should live in shared infrastructure.
Represent sensitive data as a distinct runtime and type-system value, not an ordinary string:
const email = Sensitive.pii(input.email);
const token = Sensitive.credential(accessToken);
logger.info({ email }); // emits "[REDACTED]"
url.searchParams.set("email", email); // compile-time or runtime error
request.setBaggage("token", token); // error
request.json({ email }); // allowed by an explicit route policy
The hard part is keeping the label through parsing, validation, interpolation, exceptions, queues, database objects, RPC boundaries and asynchronous callbacks. Make those transitions part of the framework, and require explicit declassification before a value enters an unsafe sink.
Sensitivity metadata should distinguish at least:
Classify fields once in request and response schemas, then generate:
This is safer than maintaining a separate list of field names in every logger. Those lists miss aliases such as secret, apiKey, credential and assertion, as well as nested values, arrays and secrets buried in free-form strings.
Do not give loggers the raw request object. Give them a deliberately lossy representation:
{
"method": "POST",
"route": "/users/:id",
"status": 400,
"request_bytes": 842,
"content_type": "application/json",
"request_id": "…"
}
Raw headers and bodies should be unavailable by default, including from exception objects. For correlation, log a keyed HMAC fingerprint rather than the original credential or identifier. Temporary raw capture needs a narrow break-glass policy, short retention, audited access and automatic deletion.
Structurally encode any user-controlled value that does reach a log. Redaction protects confidentiality; encoding stops crafted line breaks or delimiters from forging entries or attacking downstream processors.
Sensitive routes should automatically receive:
Cache-Control: no-store
Referrer-Policy: no-referrer
Reject contradictory public or s-maxage directives and sensitive headers named in Vary. Disable body capture, keep sensitive values out of cache tags and surrogate keys, and redirect a completed one-time exchange to a clean URL.
The safest URL value is often a random handle with no meaning outside a short redemption window. Make the nonce and single-use-token lifecycle a primitive: bind the handle, redeem one logical operation safely across retries, exchange it for server-side state or an HttpOnly cookie, redirect to a clean URL, and retain only a non-reversible audit fingerprint. Keep concurrency, recovery and replay policy out of individual handlers.
For especially sensitive bodies, encrypt selected fields before the request reaches the CDN or TLS-terminating gateway. A standard construction such as HPKE, used inside a protocol with replay protection and authenticated context, can leave early infrastructure with ciphertext only.
After decryption, recreate sensitivity-aware values rather than ordinary strings, or the first exception may put the plaintext straight back into a log. Keep key rotation, algorithm negotiation, payload binding, replay detection and failure handling in shared code.
A server-side BFF can hold API credentials while the browser sees only an opaque session. It should add Authorization only for allowlisted resource origins, restrict methods and paths, rate-limit operations, and strip credentials before redirects or error serialisation.
A worker can keep a token out of the main page’s JavaScript heap and attach it to requests. That makes direct extraction harder, but it is not a BFF. RFC 10017 notes that service-worker OAuth remains vulnerable to new token acquisition and browser-mediated request proxying, and does not recommend it as the general solution.
Reduce the value of a copied bearer token with DPoP or another sender constraint, non-exportable WebCrypto keys, short lifetimes, narrow audiences and per-request proofs bound to a method and URL.
This cuts down off-device replay. It does not stop malicious same-origin JavaScript asking the legitimate browser to make requests or obtain fresh credentials. “Non-exportable” means the key bytes cannot be extracted; code with the right privileges can still invoke the key.
Enforce an origin-level egress policy below application code. It should:
Cookie, Authorization, and custom credentials to unapproved origins;Putting this below application code means a forgotten Fetch wrapper or a new HTTP client cannot bypass it by accident.
Inject a unique, non-functional canary into every sensitive carrier, then exercise:
Fail the test if a canary appears anywhere it was not declared. Give each carrier a different value so the result identifies the leaking path. Now redaction is an observed property, not a configuration claim.
Leak canaries are not the honeytokens recommended by OWASP A09. A honeytoken is a trap: unexpected use suggests an attacker. A leak canary is test data: finding it in an undeclared cache, log, trace or error report proves accidental propagation. Both are useful, but they should trigger different responses.
Once sensitive data reaches logs or telemetry, removing it is harder than preventing collection. Attach retention classes to safe derived events, keep high-sensitivity data out of long-retention stores, record which processors received it, and automate cache and telemetry deletion after an incident.
You cannot guarantee deletion from an unknown intermediary, immutable backup or outside recipient. You can at least reduce how many systems have to be trusted.
For a new browser application, I would work down this list:
Secure; HttpOnly; SameSite session cookie.Authorization credential held in memory.None of this protects data that has to be revealed to a compromised page. Malicious same-origin JavaScript can read it, intercept it before protection is applied, replace framework functions or use the browser as an authenticated proxy.
The realistic goal is smaller: give the browser fewer reusable secrets, give every credential less authority and a shorter life, keep raw requests out of observability systems, and make unsafe data movement difficult to do by accident.