Back to Blog
critical SEVERITY6 min read

How Sensitive Data Exposure happens in Python web applications and how to fix it

A critical sensitive data exposure vulnerability was discovered in `nodes/google_gemini.py` where the Google Gemini API key was returned in plaintext through a web endpoint. The fix masks the token in API responses, preventing credential theft from any client that queries the token endpoint. This protects downstream users of this Node.js library from unauthorized access to their Google Gemini services.

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

Answer Summary

This is a Sensitive Data Exposure vulnerability (CWE-200/CWE-312) in a Python web application where the `get_gemini_token` endpoint in `nodes/google_gemini.py` returned a stored Google Gemini API key in plaintext from a JSON file. The fix replaces the actual token value with a masked string ("****") in the API response, ensuring credentials are never exposed through the endpoint while still indicating whether a token is configured.

Vulnerability at a Glance

cweCWE-312 (Cleartext Storage of Sensitive Information)
fixMask the token in API responses by returning "****" instead of the actual credential
riskFull API key theft enabling unauthorized Google Gemini API access
languagePython
root causeThe `get_gemini_token` endpoint returned the raw API key value from the JSON token store
vulnerabilitySensitive Data Exposure / Plaintext Credential Storage

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:

  1. A user stores their Google Gemini API key via the /academia/gemini_token POST endpoint
  2. The key is written in plaintext to academia_tokens.json on the filesystem
  3. 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:

  1. Discover the endpoint: Make a GET request to /academia/gemini_token
  2. Receive the plaintext key: The response contains {"token": "AIzaSyB...actual_key_here..."}
  3. 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:

  1. If a token exists (data.get("gemini", "") is truthy): Return "****" instead of the actual key
  2. 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.

Key Takeaways

  • The get_gemini_token endpoint 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.json file containing the plaintext Gemini API key, read via json.load(f) in get_gemini_token()
  • Sink: web.json_response({"token": data.get("gemini", "")}) in nodes/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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.