4.22 Review Sensitive Data in URL
4.22 - Review Sensitive Data in URL
Credentials and secrets in URL parameters leak through browser history, proxy logs, analytics, and Referer headers. Review login flows, password reset links, API keys in query strings, and OAuth redirects. Trace sensitive fields from forms and links to ensure they travel in request bodies or headers over HTTPS, not in GET URLs.
What This Vulnerability Is
Sensitive data exposure via URLs occurs when passwords, tokens, session identifiers, or personal data appear in the query string or path where GET semantics apply. Browsers store URLs in history. Load balancers and CDNs often log full request lines. Third-party sites may receive secrets when users follow external links from a page that included them in the URL.
The unsafe assumption is that HTTPS alone protects the data. Transport encryption hides content on the wire but not from every system that records the URL after the request arrives. This maps to CWE-598 (Use of GET Request Method With Sensitive Query Strings).
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| Feature type | Login, registration, password reset, magic links, API key auth, OAuth callbacks, shareable report URLs |
| Input entry | GET query params, bookmarkable links, email links with embedded tokens, front-end URL builders |
| Sensitive fields | Passwords, API keys, access tokens, session IDs, reset tokens, PII in query strings |
| Logging sinks | Access logs, APM, error trackers, analytics beacons that capture full request URIs |
| Weak controls | @app.route(..., methods=["GET", "POST"]) on login, [FromQuery] password, redirect URLs echoing credentials |
| Referrer leakage | Pages with secrets in the URL linked from external sites without Referrer-Policy |
Attack Payloads
Use these in authorized tests when secrets may appear in GET URLs, redirects, or Referer headers. Replace placeholders with your test values.
Pattern 1: Credentials in query string (login abuse scenario)
GET /login?user=admin&password=Secret123! HTTP/1.1
Host: app.example
The same request may be stored in proxy access logs, browser history, and analytics that capture full URIs.
Pattern 2: API key and bearer token in URL
GET /api/report?api_key=sk_live_abc123 HTTP/1.1
GET /download?access_token=eyJhbGciOiJIUzI1NiJ9... HTTP/1.1
Pattern 3: Password reset and magic-link tokens
GET /reset/confirm?token=8f3c2a1b9e7d4f6a HTTP/1.1
GET /magic-login?session=deadbeefcafebabe HTTP/1.1
Shareable links and email clients may retain the full URL indefinitely.
Pattern 4: Redirect that echoes secrets
HTTP/1.1 302 Found
Location: /dashboard?token=eyJhbGciOiJIUzI1NiJ9...
Pattern 5: Referer exfiltration to third party
<a href="https://analytics.example/landing">Continue</a>
When the prior page URL is https://app.example/home?token=SECRET, the browser may send:
Referer: https://app.example/home?token=SECRET
Pattern 6: OAuth and SSO callback tokens in query
GET /oauth/callback?code=AUTH_CODE&state=xyz HTTP/1.1
GET /sso/callback?access_token=LONG_LIVED_TOKEN HTTP/1.1
Language-Specific Sinks and Dangerous APIs
Search for bindings and builders that place secrets in the URL path or query string.
Python
pwd = request.args.get("password")
return redirect(f"/home?user={user}&token={token}")
logger.info("login url=%s", request.url) # full URI with query
Flask request.url, request.full_path; FastAPI Query() on sensitive fields; Django request.GET["password"].
Java
@GetMapping("/auth")
public void auth(@RequestParam String password) { ... }
resp.sendRedirect("/app?token=" + accessToken);
String uri = req.getRequestURI() + "?" + req.getQueryString();
log.info("request {}", uri);
Spring @RequestParam on GET login; servlet getQueryString() logged verbatim.
C
[HttpGet("auth")]
public IActionResult Auth([FromQuery] string password) { ... }
return Redirect($"/dashboard?token={accessToken}");
_logger.LogInformation("Request {Url}", Request.GetDisplayUrl());
JavaScript
const token = new URLSearchParams(location.search).get("token");
window.location = `/app?api_key=${apiKey}`;
fetch(`/proxy?url=${encodeURIComponent(secretUrl)}`);
Front-end routers that sync tokens into history.pushState query params.
Go
pass := r.URL.Query().Get("password")
http.Redirect(w, r, "/home?token="+token, http.StatusFound)
log.Printf("uri=%s", r.URL.String())
Access and APM logging
# nginx / load balancer access log line
GET /login?password=Secret123! HTTP/1.1" 200
Sample Vulnerable Code in Python
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def login(request):
# Attacker-controlled credentials may arrive in the query string on GET
user = request.GET.get("user") or request.POST.get("user")
pwd = request.GET.get("password") or request.POST.get("password")
if user and pwd and do_login(user, pwd):
# Redirect echoes username in query — may land in access logs
return HttpResponseRedirect(f"/home?user={user}")
return render(request, "login.html")
Step-by-Step Review Walkthrough
- Find GET handlers that read secrets. Search for routes that bind
password,token,api_key, orsecretfromrequest.args, query parsers, or URL search params. - Trace the Django login handler. In the sample, GET and POST share one view. A crafted link can place credentials in the URL bar, browser history, and proxy logs.
- Inspect password reset and magic-link flows. Long-lived tokens in query strings become shareable and loggable. Prefer POST exchange or opaque server-side lookup.
- Review front-end link builders. Search JavaScript for
?token=,password=, or template strings that append secrets tohrefor redirect targets. - Check logging and analytics. Access log formats, debug middleware, and exception messages must not record full URIs for auth endpoints.
- Examine Referer policy. Pages that still touch sensitive flows need
Referrer-Policy: no-referreror strict-origin when outbound links exist. - Confirm POST-only semantics. Sensitive operations should reject GET with the same parameters and return 405 or redirect to a clean URL.
Risk Impact Analysis
Credential exposure in logs. Load balancers, CDNs, and application access logs often store complete request lines. A single GET login attempt can persist passwords in log retention for months.
Browser and shared-device leakage. History, autofill, and synced browsers expose URL parameters to anyone with device access.
Referer exfiltration to third parties. When a user navigates from a page whose URL contains a token, the Referer header may send that token to external analytics or ad networks.
Caching and bookmarking. GET responses with secrets may be cached by browsers or intermediaries. Users who bookmark reset links may unknowingly store long-lived credentials.
Vulnerable Examples in Other Languages
Java
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String email = req.getParameter("email");
String password = req.getParameter("password");
if (email != null && password != null) {
authenticate(email, password);
}
req.getRequestDispatcher("/login.jsp").forward(req, resp);
}
}
@GetMapping("/reset/confirm")
public String confirmReset(@RequestParam String token, Model model) {
model.addAttribute("token", token); // token visible in browser address bar
return "reset-form";
}
C
[HttpGet("auth")]
public IActionResult Auth([FromQuery] string username, [FromQuery] string password)
{
var ok = _auth.Validate(username, password);
return ok ? Ok() : Unauthorized();
}
[HttpGet("sso/callback")]
public IActionResult SsoCallback([FromQuery] string accessToken)
{
// Token in query string — logged by proxies and sent in Referer
SignInWithToken(accessToken);
return Redirect($"/dashboard?token={accessToken}");
}
Go
func login(w http.ResponseWriter, r *http.Request) {
user := r.URL.Query().Get("user")
pass := r.URL.Query().Get("pass")
if authenticate(user, pass) {
http.Redirect(w, r, "/home", http.StatusFound)
}
}
func apiProxy(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("api_key")
resp, _ := http.Get("https://partner.example/api?key=" + key)
io.Copy(w, resp.Body)
}
Fix: Safer Patterns and Libraries to Use
Python
Accept credentials only on POST. Reject GET with password parameters. Put API keys in headers, not query strings.
from flask import Flask, request, redirect, render_template
from flask.views import MethodView
app = Flask(__name__)
class LoginView(MethodView):
def get(self):
return render_template("login.html")
def post(self):
user = request.form["user"]
pwd = request.form["password"]
if do_login(user, pwd):
return redirect("/home") # clean URL, no echoed secrets
return render_template("login.html", error="Invalid credentials"), 401
app.add_url_rule("/login", view_func=LoginView.as_view("login"), methods=["GET", "POST"])
# API keys belong in headers, not query strings
from fastapi import Header, HTTPException
async def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")):
if not valid_key(x_api_key):
raise HTTPException(status_code=401)
Important: After login or reset, redirect to URLs that strip tokens from the visible address bar.
Java
Use POST-only form login. Configure access logging to omit query strings on sensitive paths.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String email = req.getParameter("email");
String password = req.getParameter("password");
authenticate(email, password);
resp.sendRedirect(req.getContextPath() + "/home");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.getRequestDispatcher("/login.jsp").forward(req, resp);
}
// Spring Security: form login POST endpoint only
http.formLogin(form -> form.loginPage("/login").loginProcessingUrl("/login"));
C
Bind credentials from the form body. Disable GET on the same action.
[HttpPost("auth")]
public IActionResult Auth([FromForm] string username, [FromForm] string password)
{
var ok = _auth.Validate(username, password);
return ok ? RedirectToAction("Home") : Unauthorized();
}
// Response header on sensitive pages
Response.Headers["Referrer-Policy"] = "no-referrer";
Go
Read credentials only after confirming POST. Return 405 for GET with password query params.
func login(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
user := r.FormValue("user")
pass := r.FormValue("pass")
if authenticate(user, pass) {
http.Redirect(w, r, "/home", http.StatusSeeOther)
}
}
Verify During Review
- Passwords, API keys, and long-lived tokens never appear in GET query strings or shareable URLs.
- Login and registration use POST with secrets in body or
Authorizationheader over HTTPS. - Access logs, APM, and error reporting do not store full URLs for authentication endpoints.
- Redirects after sensitive operations strip credentials from the visible URL bar.
Referrer-Policyand cache headers are set where pages might still touch sensitive flows.- OpenAPI or public docs do not advertise secret-bearing query parameters.
Reference
- CWE-598: Use of GET Request Method With Sensitive Query Strings
- OWASP — Information exposure through query strings in URL
- MDN — Referrer-Policy
- Flask — HTTP methods
- FastAPI — Header parameters
- Spring Security — Form Login
- ASP.NET Core — Prevent cross-site request forgery
- Go net/http — Request handling