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, 2026•Reviewed 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

high

Unbounded Map in createLoginRateLimiter Exhausts API Memory

The console API's `createLoginRateLimiter` and `createMutationRateLimiter` stored one `Map` entry per client key with no upper bound and no expiry sweep, so an attacker rotating source addresses or identifiers could grow those maps until the Node process hit an out-of-memory crash. The fix introduces a `maxTrackedKeys` option (default 5000), a `trackedEntryLimit` sanitizer, and an `evictOldestIfFull` helper that drops the oldest tracked key while protecting the shared global counter. The rate li

high

js-yaml 4.3.1 Denial of Service: Malformed Input Hangs YAML Parser

A denial of service vulnerability in js-yaml versions 4.3.1 and earlier allows attackers to hang the YAML parser indefinitely by providing specially crafted malformed input. The fix, released in versions 4.3.2 and 3.15.2, patches the parsing logic to prevent unbounded processing. Upgrading is recommended for all applications parsing untrusted YAML data.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

critical

Updater.parseUpdate() CWE-494: Unsigned Metadata Download

The parseUpdate function in the Updater component extracted download URLs from remote server responses without cryptographic verification, enabling supply chain attacks via compromised or spoofed update servers. The fix adds strict URL validation requiring HTTPS and a trusted hostname before accepting any update metadata.

high

brace-expansion DoS: Exponential Backtracking in Nested Brace Patterns

A critical vulnerability in brace-expansion allows attackers to cause denial of service by submitting specially crafted patterns with nested braces. The exponential-time complexity in pattern expansion creates a computationally expensive path that can freeze applications processing user-controlled input.

high

CVE-2026-67213: nanoid customAlphabet Infinite Loop Fix

nanoid, a widely-used ID generator pulled in transitively through postcss and vitepress, had an infinite-loop bug in its `customAlphabet` code path before version 5.1.6. This PR pins the entire dependency tree to nanoid 5.1.16 via a pnpm override so no transitive consumer can resolve back to the vulnerable 3.3.16 release.