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:
- Web server access logs: Apache, Nginx, and other web servers log complete URLs by default
- Application server logs: The Gemini API service logs incoming requests with full URLs
- Proxy and load balancer logs: Any intermediary between the client and server captures the URL
- Browser history: If this request were made from a browser, the URL (with API key) would be saved
- Network monitoring tools: Wireshark, tcpdump, and corporate network monitors capture full URLs
- 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:
- A user installs the Wine Cellar Home Assistant integration and configures their Gemini API key
- The integration makes requests to identify wine labels from photos
- An attacker gains access to the user's home network (via compromised IoT device, guest WiFi access, or network monitoring)
- Using a packet capture tool, the attacker observes HTTP requests and extracts the URL:
https://generativelanguage.googleapis.com/...?key=AIzaSyC_captured_key - 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:
-
Header-based authentication (line 721): By changing
params={"key": self._api_key}toheaders={"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. -
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-keyheader - 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
paramswith 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 likex-goog-api-key - URL parameters are logged everywhere: server logs, proxy logs, browser history, and network captures all expose query strings by default
- The
paramsdictionary 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-keyheader 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_keyattribute in theGeminiVisionClientclass, initialized from user configuration - Sink: The
params={"key": self._api_key}parameter insession.post()atcustom_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
paramsdictionary - CWE: CWE-598 (Use of GET Request Method With Sensitive Query Strings)
- Fix: Changed authentication from
params={"key": self._api_key}toheaders={"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.