Back to Blog
critical SEVERITY8 min read

How API Key Exposure in URL Parameters happens in Python and how to fix it

The Wine Cellar Home Assistant integration exposed Gemini API keys by transmitting them as URL query parameters in HTTP requests. This critical vulnerability allowed API keys to be logged in server logs, proxy caches, and browser history. The fix moved authentication to the secure `x-goog-api-key` HTTP header, preventing credential leakage.

O
By Orbis AppSec
Published August 30, 2026Reviewed August 30, 2026

Answer Summary

API key exposure in URL parameters (CWE-598) occurs when sensitive credentials are transmitted as query strings in HTTP requests instead of secure headers. In the Wine Cellar Home Assistant integration's `gemini.py` file, the `GeminiVisionClient._call_ai()` method sent the Gemini API key as `params={"key": self._api_key}`, exposing it in logs and network traffic. The fix moved authentication to the `x-goog-api-key` HTTP header, which is not logged by default and follows Google's recommended authentication pattern.

Vulnerability at a Glance

cweCWE-598 (Use of GET Request Method With Sensitive Query Strings)
fixMove API key from URL parameters to secure HTTP header (`x-goog-api-key`)
riskAPI keys leaked in server logs, proxy caches, browser history, and network monitoring tools
languagePython
root causeAuthentication credentials transmitted as URL query parameters instead of HTTP headers
vulnerabilityAPI Key Exposure in URL Parameters

Introduction

In the Wine Cellar Home Assistant custom component, we discovered a critical API key exposure vulnerability in custom_components/wine_cellar/gemini.py at line 721. The GeminiVisionClient class was transmitting the Gemini API key as a URL query parameter in every HTTP request to Google's Gemini AI service. This seemingly small implementation detail created a significant security risk: the API key was being exposed in server logs, proxy logs, browser history, and any network monitoring tool that captured HTTP requests.

The vulnerable code looked like this:

async with session.post(
    self._api_url,
    params={"key": self._api_key},  # API key in URL!
    json=body,
    timeout=timeout,
) as resp:

This pattern affects any developer integrating with external APIs, particularly in Home Assistant integrations, IoT applications, and automation systems where API keys are used for authentication.

The Vulnerability Explained

The vulnerability exists in the _call_ai() method of the GeminiVisionClient class. Let's examine the problematic code:

async def _call_ai(
    self, model: str, body: dict, timeout_s: int
) -> dict:
    """Call Gemini AI API."""
    async with aiohttp.ClientSession() as session:
        timeout = aiohttp.ClientTimeout(total=timeout_s)
        async with session.post(
            self._api_url,
            params={"key": self._api_key},  # Line 721 - VULNERABLE
            json=body,
            timeout=timeout,
        ) as resp:

The problem is on line 721: params={"key": self._api_key}. When you pass credentials through the params dictionary in an HTTP client like aiohttp, the library appends them to the URL as query parameters. This means every request looks like:

POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=AIzaSyC_your_actual_api_key_here

Why This Is Dangerous

URL parameters are logged everywhere:

  1. Web server access logs: Apache, Nginx, and other web servers log complete URLs by default
  2. Application server logs: The Gemini API service logs incoming requests with full URLs
  3. Proxy and load balancer logs: Any intermediary between the client and server captures the URL
  4. Browser history: If this request were made from a browser, the URL (with API key) would be saved
  5. Network monitoring tools: Wireshark, tcpdump, and corporate network monitors capture full URLs
  6. Referrer headers: If the page redirects, the URL with the API key can leak to third-party sites

Attack Scenario

Imagine this scenario specific to the Wine Cellar integration:

  1. A user installs the Wine Cellar Home Assistant integration and configures their Gemini API key
  2. The integration makes requests to identify wine labels from photos
  3. An attacker gains access to the user's home network (via compromised IoT device, guest WiFi access, or network monitoring)
  4. Using a packet capture tool, the attacker observes HTTP requests and extracts the URL: https://generativelanguage.googleapis.com/...?key=AIzaSyC_captured_key
  5. The attacker now has the victim's Gemini API key and can:
    - Make unlimited API calls, exhausting the victim's quota
    - Access any data processed through the API
    - Incur charges on the victim's Google Cloud account

Even without active network monitoring, the API key could be extracted from:
- Log files on a shared Home Assistant server
- Backup files that include log directories
- Cloud logging services that aggregate Home Assistant logs

The Real-World Impact

For the Wine Cellar integration specifically, this vulnerability means:

  • Credential theft: Anyone with access to logs can steal the Gemini API key
  • Quota exhaustion: Attackers can use the stolen key to make API calls, consuming the user's quota
  • Financial impact: If the user has billing enabled, attackers can generate charges
  • Privacy violation: The attacker could analyze what wine labels the user is scanning
  • Service disruption: Google may suspend the API key if abuse is detected

This is particularly concerning for Home Assistant integrations because:
- Users often run Home Assistant on shared networks
- Log files are frequently backed up to cloud services
- Many users enable debug logging to troubleshoot issues
- Home Assistant instances are sometimes exposed to the internet

The Fix

The fix is straightforward but critical. The API key must be moved from URL parameters to HTTP headers. Here's the exact change made in the PR:

Before (vulnerable code):

async with session.post(
    self._api_url,
    params={"key": self._api_key},  # Exposed in URL
    json=body,
    timeout=timeout,
) as resp:
    resp_text = await resp.text()

    if resp.status in (401, 403):
        _LOGGER.error(
            "Gemini API key is invalid (status %s): %s", 
            resp.status, 
            resp_text[:200]  # Potentially logs sensitive info
        )

After (fixed code):

async with session.post(
    self._api_url,
    headers={"x-goog-api-key": self._api_key},  # Secure header
    json=body,
    timeout=timeout,
) as resp:
    resp_text = await resp.text()

    if resp.status in (401, 403):
        _LOGGER.error(
            "Gemini API authentication failed (status %s)", 
            resp.status
        )
        # No longer logs response text that might contain key hints

Why This Fix Works

The fix makes two critical improvements:

  1. Header-based authentication (line 721): By changing params={"key": self._api_key} to headers={"x-goog-api-key": self._api_key}, the API key is now transmitted in the HTTP header. Most web servers and proxies do not log headers by default, especially authentication headers. The URL now looks like:
    POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent
    The API key is completely absent from the URL.

  2. Reduced error logging (line 729): The error message no longer includes resp_text[:200], which could potentially contain hints about the API key or expose other sensitive information. The new message simply states "Gemini API authentication failed" with the status code.

Technical Details

The x-goog-api-key header is Google's recommended method for API key authentication. According to Google's API documentation, this header:

  • Is designed specifically for API key authentication
  • Is not logged in standard server access logs
  • Is processed before the request reaches application-level logging
  • Follows the principle of least exposure for credentials

The change is minimal (one line) but has maximum security impact. The API key goes from being visible in every log file to being transmitted securely in a header that's excluded from standard logging.

Prevention & Best Practices

To prevent API key exposure in URL parameters in your own code:

1. Always Use Headers for Authentication

When working with HTTP clients in Python, use the headers parameter for credentials:

# ❌ WRONG - Credentials in URL parameters
response = requests.get(
    "https://api.example.com/data",
    params={"api_key": secret_key}
)

# ✅ CORRECT - Credentials in headers
response = requests.get(
    "https://api.example.com/data",
    headers={"Authorization": f"Bearer {secret_key}"}
)

2. Follow API Provider Guidelines

Different APIs have different authentication patterns:

  • Google APIs: Use x-goog-api-key header
  • REST APIs: Use Authorization: Bearer {token} header
  • AWS: Use AWS Signature Version 4 (never raw keys in URLs)
  • GitHub: Use Authorization: token {github_token} header

3. Review HTTP Client Usage

Audit your codebase for these patterns:

# Dangerous patterns to search for:
params={"key": ...}
params={"api_key": ...}
params={"token": ...}
params={"secret": ...}
params={"password": ...}

Use grep or your IDE to find these patterns:

grep -r "params.*api_key" .
grep -r "params.*token" .

4. Configure Logging Carefully

Even with header-based authentication, be careful with logging:

# ❌ Don't log the entire response on auth failure
_LOGGER.error(f"Auth failed: {response.text}")

# ✅ Log only the status code
_LOGGER.error(f"Auth failed: HTTP {response.status_code}")

5. Use Static Analysis Tools

Configure tools to detect credential exposure:

  • Semgrep: Create rules to detect params with sensitive variable names
  • Bandit: Flags hardcoded credentials (use with custom rules)
  • CodeQL: Write queries to detect credential flow to URL parameters
  • Orbis AppSec: Automatically detects and fixes these patterns

Example Semgrep rule:

rules:
  - id: api-key-in-url-params
    pattern: |
      params={..., "$KEY": $VALUE, ...}
    metavariable-regex:
      metavariable: $KEY
      regex: (key|token|api_key|secret|password)
    message: "Sensitive credential in URL parameters"
    severity: ERROR

6. Security Standards Reference

This vulnerability maps to:
- CWE-598: Use of GET Request Method With Sensitive Query Strings
- OWASP API Security Top 10: API2:2023 Broken Authentication
- OWASP ASVS: V2.2.1 - Credentials are transmitted using POST over HTTPS

Key Takeaways

  • Never pass API keys through URL parameters in the GeminiVisionClient._call_ai() method or any HTTP client—always use headers like x-goog-api-key
  • URL parameters are logged everywhere: server logs, proxy logs, browser history, and network captures all expose query strings by default
  • The params dictionary in aiohttp and requests is dangerous for credentials—it appends values to the URL where they're visible and logged
  • Google's x-goog-api-key header is the correct authentication method for Gemini API, not query parameters
  • Error messages in line 729 should never include response text that might contain credential hints—log only status codes for authentication failures
  • Static analysis tools can detect when sensitive variables (named api_key, token, etc.) flow into URL parameter dictionaries

How Orbis AppSec Detected This

  • Source: The self._api_key attribute in the GeminiVisionClient class, initialized from user configuration
  • Sink: The params={"key": self._api_key} parameter in session.post() at custom_components/wine_cellar/gemini.py:721, which appends the API key to the URL query string
  • Missing control: No validation to ensure credentials are transmitted via secure headers instead of URL parameters; the code directly passed the API key to the params dictionary
  • CWE: CWE-598 (Use of GET Request Method With Sensitive Query Strings)
  • Fix: Changed authentication from params={"key": self._api_key} to headers={"x-goog-api-key": self._api_key} to prevent credential exposure in URLs and logs

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

The Wine Cellar integration's API key exposure demonstrates how a single line of code can create a critical security vulnerability. By transmitting the Gemini API key as a URL parameter instead of an HTTP header, the integration exposed credentials to every system that logs HTTP requests. The fix—moving authentication to the x-goog-api-key header—is simple but essential.

This vulnerability is a reminder that secure coding isn't just about complex cryptography or access controls. Sometimes it's about understanding the fundamentals: URL parameters are logged, headers (usually) aren't. When you're integrating with external APIs, always consult the provider's authentication documentation and use headers for credentials. Your API keys—and your users' security—depend on it.

References

Frequently Asked Questions

What is API key exposure in URL parameters?

It's a vulnerability where sensitive credentials are transmitted as query strings in URLs (e.g., `?key=secret123`), causing them to be logged in server logs, proxy caches, browser history, and network monitoring tools. This violates CWE-598 and exposes credentials to anyone with access to these logs.

How do you prevent API key exposure in URL parameters in Python?

Always transmit API keys and sensitive credentials in HTTP headers, not URL parameters. Use the `headers` parameter in libraries like `aiohttp` or `requests` (e.g., `headers={"Authorization": f"Bearer {api_key}"}` or `headers={"x-goog-api-key": api_key}`). Never include credentials in the `params` dictionary.

What CWE is API key exposure in URL parameters?

CWE-598: Use of GET Request Method With Sensitive Query Strings. This CWE covers the exposure of sensitive information through URL parameters, which are logged by web servers, proxies, and browsers by default.

Is HTTPS encryption enough to prevent API key exposure in URL parameters?

No. While HTTPS encrypts data in transit, it does not prevent URL parameters from being logged by web servers, application servers, proxies, load balancers, and browser history. These logs are often stored in plaintext and accessible to system administrators, security teams, and potential attackers who gain access to these systems.

Can static analysis detect API key exposure in URL parameters?

Yes. Static analysis tools can detect patterns where sensitive data (identified by variable names like `api_key`, `token`, `secret`) is passed to URL parameter functions. Tools like Semgrep, CodeQL, and specialized security scanners can flag these patterns with rules that match HTTP client calls with credentials in query strings.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #25

Related Articles

critical

How Hardcoded API Keys Happen in TOML Configuration Files and How to Fix Them

A hardcoded Google Maps API key was discovered in `exampleSite/config/_default/params.toml` at line 113, exposing a live credential that any attacker could extract from the repository and use to make unauthorized API calls. This critical vulnerability was automatically detected and fixed by replacing the hardcoded key with an empty placeholder, eliminating the risk of credential theft and unauthorized usage charges.

critical

How Plaintext Secret Storage Happens in Cloudflare Workers (wrangler.toml) and How to Fix It

A critical misconfiguration in `platforms/m365/wrangler.toml` left developers one copy-paste away from committing live API keys directly into git history. The fix adds an explicit warning comment blocking the `[vars]` anti-pattern and adds `.dev.vars` to `.gitignore`, ensuring secrets flow through Cloudflare's encrypted `wrangler secret` mechanism instead of plaintext config. This matters because git history is permanent — a key committed even once can be extracted long after it's "deleted."

critical

How Hardcoded API Keys Happen in JavaScript and How to Fix Them

A critical security vulnerability was discovered in `src/js/init.js` where a Bugsnag API key was hardcoded directly into client-side JavaScript, making it visible to anyone who inspects the page source or JavaScript bundle. The fix replaces the hardcoded string with an environment variable reference (`import.meta.env.VITE_BUGSNAG_API_KEY`), ensuring the key is injected at build time rather than baked into the shipped code. This pattern is one of the most common — and most avoidable — secrets exp

high

How Hardcoded API Keys happen in JavaScript and how to fix it

A critical security vulnerability was discovered in `javascripts/common.js` where Firebase API keys, auth domains, and sender IDs were hardcoded directly in client-side JavaScript. Any user who opened browser DevTools or viewed page source could extract these credentials and make unauthorized calls to the Firebase Realtime Database and Yandex Translation services. The fix moves all sensitive configuration values to environment variables, ensuring secrets never reach the client bundle.

critical

How Plaintext Credential Storage Happens in Node.js Config Files and How to Fix It

A critical vulnerability in `config.js` allowed OAuth tokens and user IDs to silently fall back to empty strings when environment variables were unset, enabling credential bypass and potential hardcoded secret exposure. The fix removes the `|| ""` fallback pattern, ensuring credentials are either properly set or explicitly `undefined`, and updates downstream checks to use truthy evaluation instead of empty-string comparison. This change closes a subtle but dangerous gap that could have allowed A

critical

How Command Injection Happens in Python Flask Applications and How to Fix It

A critical command injection vulnerability was discovered in a Flask application where `subprocess.Popen` and `subprocess.run` were called with `shell=True`, allowing attackers to execute arbitrary system commands through shell metacharacters. The fix replaces dangerous shell execution with `shlex.split()` for proper argument parsing and sets `shell=False` to prevent command injection attacks.