Definition
- URLs are logged – GET URLs are written to web server, proxy, load-balancer, CDN, and application logs in plaintext.
- URLs are leaked – they are saved in browser history, bookmarks, and can appear in the HTTP
Refererheader sent to third parties. - TLS protects transit only – HTTPS encrypts the URL in transit but does nothing about the places it is stored at endpoints.
- POST body is safer – request bodies are not written to standard access logs, not stored in history, and not leaked via referrer.
- Defense in depth – POST is necessary but not sufficient; also enforce HTTPS, mask sensitive values in app logs, and encrypt at rest.
Sending a National Bank of India customer's Aadhaar number in the URL is like writing it on the outside of an envelope: every post office and camera can read it, and it stays in the mail log. Putting it in the POST body is like sealing it inside the envelope: the courier (HTTPS) protects the whole thing, but even the handling records only note "a sealed letter was sent," not the secret inside.
Understanding the Concepts
Why is GET with sensitive data in the URL dangerous?
GET request URLs are recorded and exposed in many places even when the connection is encrypted.
GET /verify?aadhaar=1234-5678-9012 HTTP/1.1 Where this URL ends up: ┌──────────────────────────────────────────────────────┐ │ 1. Web server access log: 200 GET /verify?aadhaar=.. │ │ 2. Proxy / load-balancer log │ │ 3. CDN log │ │ 4. Application log (if request is logged) │ │ 5. Browser history and bookmarks │ │ 6. HTTP Referer header (if page links to another) │ │ 7. Browser auto-complete suggestions │ └──────────────────────────────────────────────────────┘
TLS encrypts the URL in transit so a network eavesdropper cannot read it, but it does nothing about all the endpoint storage.
How does POST with the body avoid the problem?
A POST places the data in the request body, which is not recorded by standard infrastructure.
POST /verify HTTP/1.1
Content-Type: application/json
{"aadhaar": "1234-5678-9012"}
Where the body does NOT end up:
┌──────────────────────────────────────────────────────┐
│ 1. Web server access log: 200 POST /verify (no body) │
│ 2. Browser history: POST /verify (no body) │
│ 3. Referer header: /verify (no body) │
│ 4. Bookmarks: not bookmarkable │
└──────────────────────────────────────────────────────┘
How do you implement this in a Spring Boot API?
Rajesh moves the Aadhaar from a GET parameter to a POST body at National Bank of India.
package com.bank.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/kyc")
public class KycController {
// WRONG: Aadhaar in URL
@GetMapping("/verify")
public String verifyBad(@RequestParam String aadhaar) {
// Aadhaar appears in access logs, history, referrer
return kycService.verify(aadhaar);
}
// CORRECT: Aadhaar in POST body
@PostMapping("/verify")
public String verifyGood(@RequestBody KycRequest request) {
// Aadhaar stays in the request body, not logged by default
return kycService.verify(request.aadhaar());
}
public record KycRequest(String aadhaar) {}
}
What about lookups that semantically should be GET?
Even if a read operation would normally use GET, send the sensitive identifier in a POST body or use an internal non-sensitive id.
package com.bank.controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/customer")
public class CustomerController {
// Option 1: POST body for the lookup
@PostMapping("/search")
public CustomerResponse searchByPan(@RequestBody PanSearchRequest request) {
return customerService.findByPan(request.pan());
}
// Option 2: use internal non-sensitive ID in the URL
@GetMapping("/{customerId}")
public CustomerResponse getByInternalId(@PathVariable long customerId) {
return customerService.findById(customerId);
}
public record PanSearchRequest(String pan) {}
}
What additional defenses are needed beyond POST?
POST with the body is necessary but not sufficient. Defense in depth is required.
package com.bank.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SensitiveDataDefenseChecklist {
// 1. HTTPS everywhere (never plain HTTP)
// 2. POST body for sensitive identifiers
// 3. Mask Aadhaar/PAN in application logs
// log.info("Verifying Aadhaar: ****{}", last4Digits);
// 4. Cache-Control: no-store on responses
// 5. Encrypt at rest (field-level encryption or tokenization)
// 6. Strict authorization: only authorized users can call /verify
// 7. Input validation: Aadhaar must match format \d{4}-\d{4}-\d{4}
}
How do you mask sensitive data in application logs?
Even with POST, the application might log the request body. Mask sensitive fields.
package com.bank.logging;
public class SensitiveDataMasker {
public static String maskAadhaar(String aadhaar) {
if (aadhaar == null || aadhaar.length() < 4) return "****";
return "****-****-" + aadhaar.substring(aadhaar.length() - 4);
}
public static String maskPan(String pan) {
if (pan == null || pan.length() < 4) return "****";
return "******" + pan.substring(pan.length() - 4);
}
}
// Usage in service:
// log.info("Verifying Aadhaar: {}", SensitiveDataMasker.maskAadhaar(aadhaar));
// Logs: Verifying Aadhaar: ****-****-9012
How do you set Cache-Control headers?
package com.bank.controller;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
public class KycController {
@PostMapping("/api/kyc/verify")
public ResponseEntity<KycResult> verify(@RequestBody KycRequest request) {
KycResult result = kycService.verify(request.aadhaar());
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore()) // no caching!
.body(result);
}
}
What should a senior engineer know?
- POST body is the minimum; field-level encryption or tokenization is the gold standard.
- Consider hashing the Aadhaar/PAN at rest and storing only the hash for lookups.
- Ensure that neither the WAF, API gateway, nor the load balancer logs the request body.
- Audit access to the /verify endpoint and alert on unusual patterns.
- OWASP guidelines recommend never passing sensitive data in URLs.
Visual Overview
GET (unsafe): POST (safe):
┌──────────────────────────┐ ┌──────────────────────────┐
│ GET /verify?aadhaar=1234 │ │ POST /verify │
│ │ │ Body: {aadhaar: "1234"} │
└──────────┬───────────────┘ └──────────┬───────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Logged in: │ │ Logged in: │
│ access logs ✗ │ │ access logs: no │
│ browser hist ✗ │ │ browser hist: no│
│ referer hdr ✗ │ │ referer hdr: no │
│ CDN logs ✗ │ │ CDN logs: no │
└──────────────────┘ └──────────────────┘
Aadhaar exposed! Aadhaar protected
Key Takeaway
For sensitive data like Aadhaar or PAN, use POST (or PUT) and carry the value in the request body, never in the URL. URLs are logged by web servers, proxies, load balancers, and CDNs. They are saved in browser history and bookmarks and can be leaked through the HTTP Referer header. TLS encrypts the URL in transit but does nothing about all the places it is stored in plaintext at the endpoints.
A request body is not written to standard access logs, not stored in browser history, and not leaked via the referrer. Combined with HTTPS, the body keeps the sensitive value off the easily exposed surfaces.
At the National Bank of India, Rajesh found a KYC endpoint implemented as GET /verify?aadhaar=... that had leaked full Aadhaar numbers into web server logs and browser history on shared branch terminals. He fixed it by switching to POST /verify with the Aadhaar in the JSON body, masking Aadhaar in application logs, and setting Cache-Control: no-store on the response.
The senior point is that POST is necessary but not sufficient. Also enforce HTTPS everywhere, mask Aadhaar and PAN in application logs, encrypt the data at rest with field-level encryption or tokenization, and apply strict authorization.
Remember one sentence: use POST or PUT with the sensitive value in the request body over HTTPS, and never put Aadhaar or PAN in a GET URL.
---