HTTP provides different methods for transmitting data between clients and servers. The GET method appends data to the URL as query parameters, making it visible in the address bar, while POST sends data in the request body where it's not directly visible. The choice between GET and POST matters significantly when handling sensitive information because URLs are logged, cached, and shared in ways that request bodies are not.

CWE-598 Overview

The Common Weakness Enumeration (CWE) is a list of software and hardware security weaknesses. The CWEs are numbered, and each one describes a specific type of mistake that could lead to a security vulnerability. Understanding these common weaknesses helps identify potential security problems, both for the developer and an attacker.

CWE-598 occurs when web applications use the HTTP GET method to process requests that include sensitive information in the URL query string. Sensitive information includes passwords, session tokens, API keys, personal data like email addresses or phone numbers, credit card details, or any data that should remain confidential. When this data appears in query parameters, it becomes exposed through browser history, server logs, referrer headers, and proxy caches.

This weakness is dangerous because URLs are recorded and transmitted in numerous ways that developers often overlook. Browser history stores every URL visited, including all query parameters. Web server access logs record complete URLs for every request. Proxy servers and Content Delivery Networks (CDNs) cache URLs. When users share links or click to external sites, the Referer header exposes the complete URL including query strings. Once sensitive data appears in a URL, it's leaked to multiple locations where it can be accessed by attackers, system administrators, or anyone with access to logs or browser history.

This CWE is a variant of the more general parent CWE-201: Insertion of Sensitive Information Into Sent Data.

Example 1: Password in Login URL

Applications sometimes send credentials as URL parameters instead of in a POST request body:

<form action="/login" method="GET">
    <input type="text" name="username" placeholder="Username">
    <input type="password" name="password" placeholder="Password">
    <button type="submit">Log In</button>
</form>

On the backend there is:

@app.route('/login', methods=['GET'])
def login():
    username = request.args.get('username')
    password = request.args.get('password')
    
    if verify_credentials(username, password):
        session['user_id'] = get_user_id(username)
        return redirect('/dashboard')
    else:
        return 'Invalid credentials'

This is a weakness because the password appears in the URL as /login?username=alice&password=secret123. This URL is saved in the browser's history, recorded in web server access logs, and potentially sent to other sites in the Referer header. Anyone with access to the user's browser history or the server logs can see the plaintext password. The HTML input type="password" masks the password on screen, but this gives no protection when the form uses the GET method.

Example 2: Session Tokens in Query Parameters

Applications might pass session identifiers through URL parameters for authentication:

// Generate session and redirect with token in URL
app.post('/api/auth', (req, res) => {
  const { username, password } = req.body;

  if (verifyCredentials(username, password)) {
    const sessionToken = generateToken();
    storeSession(sessionToken, username);

    // Redirect with session token in URL
    res.redirect(`/dashboard?session=${sessionToken}`);
  }
});

// Check session from query parameter
app.get('/dashboard', (req, res) => {
  const sessionToken = req.query.session;

  if (isValidSession(sessionToken)) {
    renderDashboard(res, sessionToken);
  } else {
    res.redirect('/login');
  }
});

This is a weakness because the session token appears in the URL as /dashboard?session=a8f3d9e2c1b4. This token is saved in browser history and server logs. If the user shares the dashboard URL with someone or if the URL appears in the Referer header when navigating to external sites, the session token is exposed. Attackers who obtain this token can hijack the user's session and impersonate them. Session tokens should be stored in secure, HTTP-only cookies, not in URLs.

Example 3: API Keys in GET Requests

APIs sometimes require authentication keys to be passed as query parameters:

@app.route('/api/user')
def get_user_data():
    api_key = request.args.get('api_key')
    user_id = request.args.get('user_id')
    
    if verify_api_key(api_key):
        user_data = fetch_user_data(user_id)
        return jsonify(user_data)
    else:
        return jsonify({'error': 'Invalid API key'}), 401

In the client:

fetch('https://api.example.com/api/user?api_key=abc123def456&user_id=789')
  .then(response => response.json())
  .then(data => console.log(data));

This is a weakness because the API key appears in the URL as /api/user?api_key=abc123def456&user_id=789. This key is logged by web servers, proxies, and any network monitoring tools. The API key might be cached by CDNs or browser history. If the client is a web application making this request from the browser, the key is visible in the browser's network inspector. API keys should be transmitted in HTTP headers like Authorization, never in URL parameters.

Example 4: Personal Information in Password Reset Links

Password reset functionality often includes sensitive data in URL parameters:

<?php
// Send password reset email
function sendPasswordReset($email) {
    $resetToken = bin2hex(random_bytes(32));
    $userId = getUserIdByEmail($email);
    
    storeResetToken($userId, $resetToken);
    
    $resetUrl = "https://example.com/reset_password?" . 
                "email=" . urlencode($email) . 
                "&token=" . $resetToken . 
                "&user_id=" . $userId;
    
    sendEmail($email, "Reset your password", 
              "Click here to reset: " . $resetUrl);
}

// Process password reset
function resetPassword() {
    $email = $_GET['email'];
    $token = $_GET['token'];
    $userId = $_GET['user_id'];
    
    if (verifyResetToken($userId, $token)) {
        // Show password reset form
        showResetForm($userId);
    } else {
        die("Invalid reset link");
    }
}
?>

This is a weakness because the password reset URL contains the user's email address, user ID, and reset token as query parameters. This URL is sent via email, which may be unencrypted, and appears in the browser history when clicked. The email address in the URL is unnecessary since the token alone could identify the reset request. If the user forwards the email or the URL is logged anywhere, both the email address and reset token are exposed, allowing attackers to take over the account.

What to Remember

The most important points to keep in mind about using GET with sensitive query strings are:

  • Never use GET method for requests that include passwords, API keys, session tokens, or other sensitive credentials
  • URLs with query parameters are logged in browser history, web server logs, proxy caches, and CDN logs, exposing any sensitive data they contain
  • The Referer header sends complete URLs including query parameters to external sites when users navigate away from your application
  • Use POST method for sensitive data, which places information in the request body instead of the URL where it's less likely to be logged or leaked
  • Session identifiers should be stored in secure, HTTP-only cookies, not passed as URL parameters
  • API authentication should use Authorization headers or other header-based mechanisms, not query string parameters

Want to learn more about security weaknesses? I'm working through the CWE list and doing writeups for security challenges. Follow along for more articles like this one.