Back to Blog
critical SEVERITY8 min read

How API Key Exposure in Request Bodies happens in React and how to fix it

The Chatbot component in gitforme was transmitting Azure OpenAI API keys inside JSON request bodies, causing them to be logged by servers, proxies, and middleware. By moving the apiKey from requestBody.apiKey to an Authorization header, credentials are now protected from persistence in generic request logging infrastructure.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 9, 2026•Reviewed September 9, 2026

Answer Summary

API Key Exposure in Request Bodies is a credential disclosure vulnerability (CWE-319) in React/JavaScript where sensitive tokens sent in HTTP POST bodies get logged by servers, proxies, and debugging tools. In gitforme's Chatbot.jsx at line 120, the Azure OpenAI apiKey was included in requestBody.apiKey and sent as JSON, making it visible in browser DevTools, server logs, and any intermediate systems. The fix moves the API key to an Authorization: Bearer header, which is typically excluded from body logging and treated as sensitive metadata by infrastructure.

Vulnerability at a Glance

cweCWE-319 (Cleartext Transmission of Sensitive Information)
fixMove apiKey from requestBody to Authorization: Bearer header
riskCredentials logged in server request logs, proxy logs, browser DevTools, and debug output
languageJavaScript (React/JSX)
root causeSensitive API key included in JSON request body instead of protected header
vulnerabilityAPI Key Exposure in Request Bodies

Title: How API Key Exposure in Request Bodies happens in React and how to fix it


VULNERABILITY_AT_A_GLANCE: API Key Exposure in Request Bodies (CWE-319) in React/JavaScript — Azure OpenAI credentials sent in JSON bodies were being logged by servers and proxies. Fixed by moving apiKey from requestBody to Authorization: Bearer header in Chatbot.jsx:123.


Introduction

In the gitforme repository, we discovered a critical credential exposure vulnerability in gitforme/src/components/Chatbot.jsx that put Azure OpenAI API keys at risk of compromise. The useChat hook's message submission logic (around line 120) was packing sensitive credentials directly into JSON request bodies, creating a dangerous paper trail across multiple systems.

The problematic pattern was deceptively simple: when users provided custom Azure OpenAI configuration, the code added their apiKey to the same object as chat messages and parameters:

// VULNERABLE CODE (lines 120-127, before fix)
if (azureEndpoint && apiKey && deployment) {
  requestBody.azureEndpoint = azureEndpoint;
  requestBody.apiKey = apiKey;  // ← CRITICAL: API key in body
  requestBody.deployment = deployment;
  requestBody.apiVersion = apiVersion;
}

This meant every chat request carried the raw API key through infrastructure explicitly designed to log and inspect request payloads. For developers building AI-powered applications, this pattern represents a common but serious architectural mistake: conflating application data with authentication credentials.


The Vulnerability Explained

The Specific Code Pattern

The vulnerability resided in the useChat custom hook within Chatbot.jsx. When constructing the requestBody object for the /api/chat endpoint, the code conditionally added Azure configuration:

// BEFORE (vulnerable)
const requestBody = {
  messages: updatedMessages,
  model: selectedModel,
  systemPrompt,
  temperature,
};

if (azureEndpoint && apiKey && deployment) {
  requestBody.azureEndpoint = azureEndpoint;
  requestBody.apiKey = apiKey;        // Line 124: Key added to body
  requestBody.deployment = deployment;
  requestBody.apiVersion = apiVersion;
}

const response = await fetch("https://gitforme-bot.onrender.com/api/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(requestBody),  // Key serialized to JSON
});

Why This Creates Multiple Exposure Vectors

1. Server-Side Request Logging
Most web servers and application frameworks log incoming request bodies for debugging, monitoring, and audit purposes. Express.js with morgan, Python's logging middleware, and cloud platforms like AWS API Gateway all capture request payloads by default. The Azure OpenAI key would appear verbatim in these logs.

2. Proxy and Load Balancer Logs
Infrastructure components between client and server—NGINX, HAProxy, Cloudflare, AWS ALB—often log request bodies for traffic analysis. These systems typically have different retention policies and access controls than application databases.

3. Browser Developer Tools
Any user could open Chrome DevTools, navigate to the Network tab, and see their own API key in the request payload. This creates social engineering risks and complicates credential rotation.

4. Error Reporting and APM Tools
Services like Sentry, Datadog, and New Relic capture request context when errors occur. If an exception happened during chat processing, the API key would be bundled into the error report.

Real-World Attack Scenario

Consider this chain of events in the gitforme application:

  1. A user configures their own Azure OpenAI deployment with a corporate API key
  2. They send a chat message, which triggers useChat's sendMessage function
  3. The request passes through Cloudflare (logged), reaches the Render-hosted backend (logged by gitforme-bot.onrender.com), and potentially triggers an error
  4. An error report sent to Sentry contains the full request body with apiKey
  5. A compromised Sentry account or insider threat now has access to the Azure OpenAI key
  6. The attacker uses the key to exhaust quota, access proprietary model deployments, or pivot to other Azure resources

The key insight: HTTPS encrypts data in transit, but it does not control what happens after decryption. The receiving system and all intermediaries see the plaintext body.


The Fix

The Specific Changes

The fix modifies gitforme/src/components/Chatbot.jsx with a surgical three-line change that preserves all functionality while eliminating the exposure:

@@ -120,16 +120,19 @@ const useChat = () => {
       };

       // ✅ Only include Azure creds if all 3 are present
+      const headers = { "Content-Type": "application/json" };
       if (azureEndpoint && apiKey && deployment) {
         requestBody.azureEndpoint = azureEndpoint;
-        requestBody.apiKey = apiKey;
         requestBody.deployment = deployment;
         requestBody.apiVersion = apiVersion;
+        // 🔒 Send the API key via the Authorization header rather than the
+        // JSON body so it is not persisted alongside generic request-body logs.
+        headers.Authorization = `Bearer ${apiKey}`;
       }

       const response = await fetch("https://gitforme-bot.onrender.com/api/chat", {
         method: "POST",
-        headers: { "Content-Type": "application/json" },
+        headers,
         body: JSON.stringify(requestBody),
       });

How This Solves the Problem

Aspect Before (Vulnerable) After (Fixed)
API Key Location requestBody.apiKey in JSON payload Authorization: Bearer <key> header
Server Logging Captured in request body logs Typically excluded or redacted in header logs
DevTools Visibility Visible in "Payload" tab Visible in "Request Headers" section
Proxy Logging Logged by body-inspecting middleware Usually filtered by header-sensitive configurations

The fix leverages a fundamental HTTP architectural principle: headers and bodies have different semantics and handling conventions. Authentication credentials belong in headers because:

  1. Semantic correctness: The Authorization header was specifically designed for this purpose (RFC 7235)
  2. Infrastructure expectations: Logging systems routinely redact or exclude Authorization headers from output
  3. Security boundaries: Headers are processed by authentication layers before reaching application logic

Notably, the fix maintains backward compatibility: the backend can continue reading azureEndpoint, deployment, and apiVersion from the body while extracting the key from the header.


Key Takeaways

  • Never place apiKey in requestBody objects sent via fetch() or XMLHttpRequest: The Chatbot.jsx vulnerability demonstrates that even seemingly temporary request objects create persistent exposure through logging infrastructure.

  • The Authorization header is the standard location for bearer tokens: The fix uses headers.Authorization = \Bearer ${apiKey}`` following RFC 6750, which infrastructure tools recognize and typically protect.

  • Conditional credential inclusion requires careful header construction: The fix creates a mutable headers object before the conditional block, allowing Authorization to be added only when Azure credentials are present—preserving the original "only include if all 3 are present" logic.

  • HTTPS is necessary but not sufficient for credential protection: Transport encryption prevents network eavesdropping, but the receiving system's logging behavior determines actual exposure risk.

  • Review all fetch() calls in React applications for body inspection: Search for patterns where variables named key, token, secret, or password appear in object literals passed to JSON.stringify() or body: properties.


How Orbis AppSec Detected This

Source: User input through React state variables apiKey (controlled by setApiKey in component state)

Sink: The fetch() call to https://gitforme-bot.onrender.com/api/chat in gitforme/src/components/Chatbot.jsx:127 where requestBody.apiKey was serialized into the JSON payload

Missing control: No transformation of the credential from body content to header metadata; absence of Authorization header usage for bearer token transmission

CWE: CWE-319 (Cleartext Transmission of Sensitive Information) — with secondary classification as CWE-532 (Insertion of Sensitive Information into Log File)

Fix: Replaced requestBody.apiKey = apiKey with headers.Authorization = \Bearer ${apiKey}``, removing the key from the logged request body while maintaining authentication capability

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 Chatbot.jsx vulnerability illustrates how a single line—requestBody.apiKey = apiKey—can cascade into systemic credential exposure across multiple systems. The fix demonstrates that security improvements need not disrupt functionality: by understanding HTTP semantics and respecting the boundary between headers and bodies, we eliminated the exposure while preserving all Azure OpenAI integration capabilities.

For developers building AI-powered applications, this case serves as a reminder that credential handling deserves architectural attention. The convenience of packing all parameters into a single request object must be weighed against the reality of how infrastructure processes those requests. When in doubt: headers for identity, bodies for data.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #63

Related Articles

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.

critical

JdbcSinkFunction.invoke() SQL Injection via Unvalidated Identifiers

The `JdbcSinkFunction.invoke()` method in Lacus's RTC engine built SQL INSERT statements with `String.format()`, placing database name, table name, and column names directly into the query string without validation. Although the actual row values used parameterized `?` placeholders, the identifiers themselves were injectable, letting a crafted sink configuration break out of the intended INSERT statement. The fix adds a strict allowlist regex that rejects any identifier containing characters out