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
•Technically reviewed by Anupam Mediratta•Published August 30, 2026•Reviewed 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.

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #25

Related Articles

critical

ARTIFICIAL_ANALYSIS_API_KEY: Unvalidated os.getenv in Model Sync

A model-catalogue sync utility read the `ARTIFICIAL_ANALYSIS_API_KEY` environment variable with a bare `os.getenv()` call and passed whatever it found straight into `fetch_intelligence_from_api()`. Any malformed value — a trailing newline from an `--env-file`, a whitespace-only placeholder, or a pasted `Bearer …` prefix — was treated as a valid credential and forwarded into an outbound HTTP request, where it could surface in CI error output. The fix normalizes the value, validates it against a s

critical

redmine_drawio View Hook Inlines Base64 Redmine API Keys

The Redmine drawio plugin's body-bottom view listener embedded the logged-in user's Redmine REST API key into client-side JavaScript on every wiki and issue page, "protected" only by Base64 encoding of the reversed string. Any script on the page — or anyone with browser developer tools, a cached copy of the HTML, or a proxy log — could decode it in one line and act as that user through the Redmine REST API. The fix removes the embedded credential from the rendered page entirely; the plugin's dia

critical

Yandex Translate API Key Leaked via URL Query Parameter

The `translateYandex()` helper built its request URL by interpolating the caller-supplied API key directly into the query string, meaning every call leaked the credential into server access logs, proxy logs, and any Referer header sent by intermediaries. The fix switches the request from a GET with the key in the URL to a POST with the key in the request body via `URLSearchParams`, removing the credential from any URL-logging surface entirely.

critical

Google Generative AI Keys Exposed in Client-Side Fetch URLs

A client-side AI model discovery utility was embedding Google Generative AI API keys directly in fetch request URLs, making them visible to any user inspecting network traffic or browser DevTools. The fix moves the key from the URL query parameter to a secure HTTP header, eliminating exposure while maintaining API authentication.

critical

How credential leakage through console logging happens in JavaScript browser extensions and how to fix it

A browser extension's `src/background/credentials.js` printed the full Strava authentication cookie string — including a signed JWT and CloudFront-Signature values — straight into the extension console via `console.debug`. Anyone who could open DevTools on the background page (or any tooling that scraped the console) could copy a live session and impersonate the user. The fix replaces the credential payload in both log statements with `Boolean(credentials)` and strips a realistic-looking JWT out

critical

How Hardcoded Secrets Compromise Authentication in JavaScript and How to Fix It

A critical vulnerability in `Tool/QuantumultX/Rewrite/RRSP.js` exposed hardcoded API authentication credentials—a TOKEN and UMID device identifier—directly in source code. Anyone with repository access could extract these credentials to impersonate the legitimate user and gain full account access to the RRTV API service. The fix replaced hardcoded secrets with empty placeholders, forcing users to manually configure credentials through secure channels.