Introduction
The nodes/google_gemini.py file handles API key management for Google Gemini integration, but a critical flaw in the get_gemini_token function at line 29 created a direct credential exposure risk. The endpoint /academia/gemini_token was designed to check whether a token was configured, but instead it returned the actual plaintext API key to any client that made the request.
This vulnerability is particularly dangerous because it exists in a Node.js library consumed by downstream users. Any application integrating this package would unknowingly expose their Google Gemini API credentials through a simple HTTP GET request—no authentication required, no access controls enforced.
The Vulnerability Explained
The vulnerable code path works like this:
- A user stores their Google Gemini API key via the
/academia/gemini_tokenPOST endpoint - The key is written in plaintext to
academia_tokens.jsonon the filesystem - When the GET endpoint is called, the full API key is read from the file and returned in the JSON response
Here's the vulnerable code:
async def get_gemini_token(request):
try:
if os.path.exists(TOKENS_FILE):
with open(TOKENS_FILE, "r") as f:
data = json.load(f)
return web.json_response({"token": data.get("gemini", "")})
except: pass
return web.json_response({"token": ""})
The critical line is:
return web.json_response({"token": data.get("gemini", "")})
This returns the raw value of the gemini key from the JSON file—the actual API key in full plaintext.
Attack Scenario
An attacker targeting an application using this library could:
- Discover the endpoint: Make a GET request to
/academia/gemini_token - Receive the plaintext key: The response contains
{"token": "AIzaSyB...actual_key_here..."} - Abuse the key: Use the stolen Google Gemini API key to make unlimited API calls, potentially incurring significant charges or accessing sensitive AI-generated content
This is a 2-step attack chain with minimal complexity:
- Step 1: Access the unprotected endpoint (no authentication required)
- Step 2: Extract the API key from the JSON response
The impact is severe: unauthorized access to Google Gemini services, potential financial abuse through API call charges, and exposure of any data processed through the Gemini API.
Why the Storage is Also Problematic
The academia_tokens.json file stores the key without any encryption:
{
"gemini": "AIzaSyB...plaintext_api_key..."
}
An attacker with filesystem access (through a path traversal vulnerability, server compromise, or misconfigured backups) could also directly read this file. However, the more immediate risk is the endpoint that actively serves this credential to any requester.
The Fix
The fix applied in nodes/google_gemini.py at line 29 masks the token value in the API response:
Before (Vulnerable):
return web.json_response({"token": data.get("gemini", "")})
After (Fixed):
return web.json_response({"token": "****" if data.get("gemini", "") else ""})
How This Solves the Problem
The fix implements token masking with a simple but effective ternary expression:
- If a token exists (
data.get("gemini", "")is truthy): Return"****"instead of the actual key - If no token is configured (empty string): Return an empty string
""
This preserves the endpoint's legitimate functionality—allowing the UI to check whether a token has been configured—while completely eliminating the credential exposure. The client can still determine "yes, a token is set" or "no, no token is configured" without ever receiving the actual secret.
The change is minimal (one line) and behavior-preserving for valid use cases. Any frontend code that only needs to know whether a token exists will continue to work correctly. Only code that was improperly relying on reading back the full token will be affected—and that's exactly the attack vector being closed.
Prevention & Best Practices
1. Never Return Secrets Through API Endpoints
API keys, passwords, and tokens should be write-only from the client's perspective. Endpoints should only confirm existence or validity, never return the raw value.
2. Encrypt Credentials at Rest
Even with the endpoint fix, the academia_tokens.json file still contains plaintext credentials. A defense-in-depth approach would encrypt the key before writing:
from cryptography.fernet import Fernet
# Encrypt before storing
cipher = Fernet(encryption_key)
encrypted_token = cipher.encrypt(api_key.encode())
3. Use Environment Variables or Secret Managers
Instead of JSON files, prefer:
- Environment variables (os.environ.get("GEMINI_API_KEY"))
- Secret management services (HashiCorp Vault, AWS Secrets Manager)
- OS-level credential stores
4. Implement Authentication on Sensitive Endpoints
The /academia/gemini_token endpoint should require authentication before returning any information about stored credentials.
5. Apply the Principle of Least Privilege
Even if a token check endpoint is needed, it should return the minimum information necessary—a boolean {"configured": true} rather than any representation of the token.
Relevant Standards
- OWASP: A02:2021 – Cryptographic Failures
- CWE-312: Cleartext Storage of Sensitive Information
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Key Takeaways
- The
get_gemini_tokenendpoint was functioning as a credential oracle—any client could retrieve the full API key with a single GET request - Token presence checks should never return the token itself—the
"****"masking pattern preserves UX while eliminating exposure - Plaintext JSON storage of API keys (
academia_tokens.json) creates a secondary attack surface beyond the endpoint itself - One-line fixes can close critical vulnerabilities—the ternary
"****" if data.get("gemini", "") else ""completely eliminates the data leak - Libraries that store credentials create inherited risk—all downstream consumers of this package were unknowingly exposing their Gemini API keys
How Orbis AppSec Detected This
- Source: The
academia_tokens.jsonfile containing the plaintext Gemini API key, read viajson.load(f)inget_gemini_token() - Sink:
web.json_response({"token": data.get("gemini", "")})innodes/google_gemini.py:29— returning the raw credential in the HTTP response - Missing control: No token masking, encryption, or access control between reading the stored credential and returning it in the API response
- CWE: CWE-312 (Cleartext Storage of Sensitive Information)
- Fix: Replace the plaintext token value with a masked string
"****"in the API response while preserving the ability to check token configuration status
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
This vulnerability demonstrates how a seemingly helpful feature—an endpoint to check your configured API key—can become a critical security flaw when it returns the actual credential instead of just confirming its existence. The fix is elegant in its simplicity: one line of code that masks the token while preserving all legitimate functionality.
For developers building similar credential management features, remember: secrets should flow in one direction only. Accept them, store them securely, use them internally, but never echo them back through any interface. The get_gemini_token endpoint now correctly serves as a configuration status check rather than a credential dispensary.