URLs appear in browser history, server logs, analytics, and Referer headers. Sensitive data in URLs can be accidentally exposed to many parties beyond the intended recipient.
Key Takeaways
- 1URLs are logged, cached, and shared in many places
- 2Never put passwords, API keys, or session tokens in URLs
- 3Use POST bodies or headers for sensitive data
- 4Detect sensitive patterns before sharing or logging URLs
- 5Scrub sensitive parameters from logs and analytics
"Sensitive data should never be transmitted via URLs as they can be cached by the browser, logged by web servers, and sent in the Referer header to third-party sites."
Where URLs Are Exposed
Before putting sensitive data in URLs, you need to understand just how many places those URLs end up. URLs flow through numerous systems, each representing a potential data leak. The table below maps exposure points to the parties who can access that data.
| Location | Who Sees It | Risk Level |
|---|---|---|
| Browser history | Anyone with device access | High |
| Server logs | Ops team, log aggregators | High |
| Referer header | Linked external sites | Medium |
| Shared bookmarks | Collaborators | Medium |
| Analytics | Marketing, third parties | Medium |
| Proxy logs | IT, network admins | Medium |
The Referer header is particularly insidious: when users click a link on your page, the full URL (including query parameters) is sent to the destination site. Third-party analytics scripts also capture URLs, potentially transmitting sensitive data to external services. Understanding these exposure vectors helps you make informed decisions about data placement.
Detection is the first step toward prevention. Let's look at how to identify sensitive parameters programmatically.
Sensitive Parameter Patterns
Sensitive parameters typically fall into recognizable categories: authentication credentials, personal data, and cryptographic keys. By maintaining a list of common sensitive parameter names, you can automatically flag URLs that may contain secrets before they're logged or shared.
// Common sensitive parameter names
const SENSITIVE_PARAMS = [
// Authentication
'password', 'passwd', 'pwd', 'secret',
'api_key', 'apikey', 'api-key',
'access_token', 'accesstoken', 'token',
'auth', 'authorization', 'bearer',
'session', 'sessionid', 'sid',
// Personal data
'ssn', 'social_security',
'credit_card', 'cc', 'cvv',
'dob', 'date_of_birth',
// Keys and secrets
'private_key', 'privatekey',
'client_secret', 'app_secret',
'signing_key', 'encryption_key',
];
function containsSensitiveParam(url) {
try {
const parsed = new URL(url);
for (const [key] of parsed.searchParams) {
const lowerKey = key.toLowerCase();
if (SENSITIVE_PARAMS.some(s => lowerKey.includes(s))) {
return { sensitive: true, param: key };
}
}
return { sensitive: false };
} catch {
return { sensitive: false };
}
}The containsSensitiveParam function scans URL parameters against a list of known sensitive names. The check uses includes rather than exact matching to catch variations like user_password or apikey_v2. This approach catches most common cases but may produce false positives for legitimate parameters.
Parameter names alone don't tell the whole story. Some sensitive data can be identified by the format of its value, regardless of the parameter name.
Pattern Detection
Value-based detection complements name-based detection by identifying secrets that may be in innocuously-named parameters. API keys, JWTs, and other tokens have distinctive formats that can be matched with regular expressions, even when the parameter name gives no indication of sensitivity.
// Detect sensitive values by pattern
const SENSITIVE_PATTERNS = [
// API keys (various formats)
/^[a-z]{2,10}_[a-zA-Z0-9]{20,50}$/, // prefix_key format
/^sk_live_[a-zA-Z0-9]{24,}$/, // Stripe secret key
/^pk_live_[a-zA-Z0-9]{24,}$/, // Stripe public key
/^AKIA[0-9A-Z]{16}$/, // AWS access key
// Tokens
/^eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/, // JWT
// Passwords (simple check)
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/, // Password-like
// Credit cards
/^\d{13,19}$/, // Card number length
];
function looksLikeSensitiveValue(value) {
return SENSITIVE_PATTERNS.some(pattern => pattern.test(value));
}These patterns match common credential formats. The JWT pattern matches the distinctive three-part base64url structure. The AWS access key pattern matches the AKIA prefix followed by 16 alphanumeric characters. Combining name and value detection provides comprehensive coverage for sensitive data identification.
Once you can detect sensitive data, the next step is preventing its exposure in logs and analytics.
Log Scrubbing
Log scrubbing replaces sensitive parameter values with placeholders before URLs are written to logs, analytics, or error tracking systems. This approach lets you maintain useful debugging information while preventing credential exposure. Apply scrubbing at the logging layer so all URL logging benefits automatically.
// Scrub sensitive params from URLs before logging
function scrubUrl(url) {
try {
const parsed = new URL(url);
for (const [key] of [...parsed.searchParams]) {
const lowerKey = key.toLowerCase();
if (SENSITIVE_PARAMS.some(s => lowerKey.includes(s))) {
parsed.searchParams.set(key, '[REDACTED]');
}
}
return parsed.href;
} catch {
return url;
}
}
// Usage
console.log(scrubUrl('https://api.com/auth?token=abc123&user=john'));
// https://api.com/auth?token=[REDACTED]&user=johnThe scrubUrl function iterates through all URL parameters and replaces values of sensitive parameters with [REDACTED]. Using [...parsed.searchParams] creates a copy of the parameters, avoiding mutation issues during iteration. This function preserves the URL structure and non-sensitive parameters for debugging.
Scrubbing is a mitigation, not a solution. The best approach is to never put sensitive data in URLs in the first place. Here are secure alternatives for common use cases.
Secure Alternatives
Each type of sensitive data has a more secure transport mechanism. The table below provides drop-in replacements for common patterns of URL parameter misuse. These alternatives prevent data from appearing in logs, browser history, and referrer headers.
| Instead Of | Use | Why |
|---|---|---|
| ?password=... | POST body | Not logged, not in history |
| ?api_key=... | Authorization header | Headers less visible than URLs |
| ?token=... | Cookie (HttpOnly) | Browser handles securely |
| ?session=... | Cookie (HttpOnly, Secure) | Not exposed in Referer |
POST bodies are the go-to replacement for form data like passwords. HTTP headers work well for API authentication since they're less visible than URL parameters in logs and never appear in browser history. Cookies with the HttpOnly and Secure flags provide the strongest protection for session management, preventing both JavaScript access and transmission over unencrypted connections.