A reusable access token grants whoever possesses it some defined access. Putting it in a URL exposes that capability to systems that routinely copy URLs: access logs, monitoring records, browser navigation history, bookmarks, and shared links. The exact exposure depends on the request and policy, but a secret-looking query parameter is not automatically protected by its name. Treat transport placement as part of the credential’s disclosure boundary.
For OAuth bearer-token requests, the Authorization header is the recommended general mechanism. HTTPS remains essential: moving a value from the query to a header does not encrypt it, and TLS protects request data in transit to its termination point. After termination, applications and intermediaries can still inspect it. A header keeps the value out of the request URL, rather than granting immunity from logging or diagnostic capture.
Distinguish reduced exposure from secrecy
Some access logs record only method, request target, and status. In that format, a header token does not appear in the target, while a query token does. Other systems capture request headers, so Authorization needs explicit handling in log, trace, and error-reporting configuration. Verify what the actual pipeline stores instead of assuming that headers bypass every logging layer. Avoid exposing real credentials while performing that inspection.
Referrer-Policy can reduce onward source-URL disclosure, but does not remove a query already sent to the destination, erase browser records, or rewrite copied links. Likewise, Cache-Control: no-store is not a general instruction to purge access logs or navigation history. Moving a token to a fragment changes what is sent in an HTTP request, but still leaves a value accessible to page scripts and browser URL state; it is not a universal credential-storage solution.
When it breaks
A team may replace query authentication with a header and leave old token-bearing URLs in reports or shared bookmarks. The transport change does not undo that exposure. Investigate the affected copies and invalidate exposed reusable credentials where the system supports it. Another failure is a supposedly temporary link whose capability remains useful to anyone who obtains it during its validity period. Short lifetime reduces that window without making disclosure harmless.
Signed URLs can be an intentional design for narrowly scoped resource access when the chosen client flow requires them. Their allowed resource, operation, expiry, and handling need explicit review. Expiration is not the same as single use: preventing reuse before expiry requires a mechanism that actually enforces consumption. A webhook or browser navigation does not automatically justify putting an unrestricted reusable API credential into a query; choose an authentication flow suitable for the real client.
Check it quickly
Save this as token-placement.py, run python3 token-placement.py --serve, and open its printed page. Python 3.14.4 and Chrome 152 were used. Every value is the literal synthetic marker DEMO-NOT-A-SECRET; never substitute a real credential. The loopback server is an observation fixture, not an authentication service. It records request targets and whether a demonstration Authorization header was present, without recording that header’s value. Control-C closes it.
import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Event, Thread
from urllib.parse import urlsplit
from urllib.request import Request, urlopen
# Synthetic demonstration value only; this server is not an authentication service.
demo = "DEMO-NOT-A-SECRET"
events = []
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
path = urlsplit(self.path).path
if path == "/events":
body = json.dumps(events).encode()
kind = "application/json"
elif path == "/echo":
entry = {"request_target": self.path,
"authorization_present": self.headers.get("Authorization") == "Bearer " + demo,
"referer": self.headers.get("Referer")}
events.append(entry)
body = json.dumps(entry).encode()
kind = "application/json"
else:
events.append({"request_target": self.path, "document": True})
body = ("""<!doctype html><meta charset="utf-8"><title>Token placement lab</title>
<h1>A clean referrer does not erase the request URL</h1><pre id="result">Running...</pre><script>
const demo=""" + json.dumps(demo) + """;
async function run() {
const query=await (await fetch('/echo?access_token='+demo)).json();
const header=await (await fetch('/echo',{headers:{Authorization:'Bearer '+demo}})).json();
await new Promise(resolve=>requestAnimationFrame(resolve));
const resourceUrls=performance.getEntriesByType('resource').map(entry=>entry.name).filter(name=>name.includes('/echo'));
window.labResult={page:location.href,query,header,resourceUrls};
document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2);
}
run().catch(error=>{window.labError=String(error);});
</script>""").encode()
kind = "text/html; charset=utf-8"
self.send_response(200)
self.send_header("Content-Type", kind)
self.send_header("Content-Length", str(len(body)))
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
server = HTTPServer(("127.0.0.1", 0), Handler)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
origin = f"http://127.0.0.1:{server.server_port}"
try:
with urlopen(origin + "/echo?access_token=" + demo, timeout=5) as response:
query = json.load(response)
with urlopen(Request(origin + "/echo", headers={"Authorization": "Bearer " + demo}), timeout=5) as response:
header = json.load(response)
assert demo in query["request_target"] and query["authorization_present"] is False
assert demo not in header["request_target"] and header["authorization_present"] is True
events.clear()
print(json.dumps({"app_url": origin + "/?access_token=" + demo,
"wire_placement_checks": "PASS", "real_credentials_used": False}), flush=True)
if "--serve" in sys.argv:
Event().wait()
except KeyboardInterrupt:
pass
finally:
server.shutdown()
thread.join(timeout=5)
server.server_close()
The page makes one request with an access_token query and another with an Authorization header. Both have null received Referer values because the page sends no-referrer. Nevertheless, the query request’s target contains the marker, while the header request’s target is simply /echo. The browser’s resource timing URLs show the same distinction. The page address itself also contains the marker, so its current navigation entry retains that URL.
Those results were checked against both browser state and the server’s synthetic event list. The Python-only probe separately confirms the request-target difference. The experiment does not test production authentication, TLS termination, log redaction, or signed-link consumption. Apply the observation to the actual client and logging system: keep reusable credentials out of URL surfaces where possible, deliberately protect the remaining credential-bearing fields, and verify the storage and sharing paths that continue after the request finishes.
Sources
- RFC 6750 OAuth 2.0 Bearer Token Usage, Authorization header use, query-method limitations, and TLS requirements, checked 2026-09-13.
- OWASP information exposure through URL query strings, URL persistence and disclosure through logs and browser records, checked 2026-09-13.
- MDN Referrer-Policy, what suppressing referrers does and does not control, checked 2026-09-13.