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:
- A user configures their own Azure OpenAI deployment with a corporate API key
- They send a chat message, which triggers
useChat'ssendMessagefunction - The request passes through Cloudflare (logged), reaches the Render-hosted backend (logged by
gitforme-bot.onrender.com), and potentially triggers an error - An error report sent to Sentry contains the full request body with
apiKey - A compromised Sentry account or insider threat now has access to the Azure OpenAI key
- 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:
- Semantic correctness: The
Authorizationheader was specifically designed for this purpose (RFC 7235) - Infrastructure expectations: Logging systems routinely redact or exclude
Authorizationheaders from output - 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.
Prevention & Best Practices
For React/JavaScript Applications
- Never mix secrets with application data
```javascript
// ❌ BAD: Secret in data object
const body = { message, apiKey: 'sk-...' };
// ✅ GOOD: Separate concerns
const body = { message };
const headers = { 'Authorization': Bearer ${apiKey} };
```
-
Use environment-specific credential handling
- Development: Proxy requests through a backend that injects credentials
- Production: Use token exchange patterns (OAuth 2.0, managed identities) -
Implement request sanitization in backend logging
javascript // Express.js example: redact sensitive headers AND body fields const morgan = require('morgan'); morgan.token('body', (req) => { const sanitized = { ...req.body }; delete sanitized.apiKey; delete sanitized.password; return JSON.stringify(sanitized); }); -
Audit third-party libraries
Thegitformeapplication uses a customuseChathook. When using libraries likeopenaior@azure/openai, verify they don't expose keys in error messages or debug output.
Detection Tools
| Tool | Capability | Rule/Pattern |
|---|---|---|
| Semgrep | Detects API keys in fetch bodies | pattern: $BODY.apiKey = $KEY with fetch(..., {body: JSON.stringify($BODY)}) |
| GitHub Secret Scanning | Detects committed Azure keys | Azure OpenAI key patterns |
| ESLint | Custom rule for fetch body inspection | no-sensitive-body-properties |
| Orbis AppSec | Full data-flow analysis | Tracks apiKey from source to sink |
Security Standards
- CWE-319: Cleartext Transmission of Sensitive Information
- CWE-532: Insertion of Sensitive Information into Log File
- OWASP API Security Top 10 2023: API2:2023 — Broken Authentication
- OWASP Cheat Sheet: Logging Cheat Sheet — specifically "Data to Exclude" section
Key Takeaways
-
Never place
apiKeyinrequestBodyobjects sent viafetch()orXMLHttpRequest: TheChatbot.jsxvulnerability demonstrates that even seemingly temporary request objects create persistent exposure through logging infrastructure. -
The
Authorizationheader is the standard location for bearer tokens: The fix usesheaders.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
headersobject before the conditional block, allowingAuthorizationto 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 namedkey,token,secret, orpasswordappear in object literals passed toJSON.stringify()orbody: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.
References
- CWE-319: Cleartext Transmission of Sensitive Information — https://cwe.mitre.org/data/definitions/319.html
- CWE-532: Insertion of Sensitive Information into Log File — https://cwe.mitre.org/data/definitions/532.html
- OWASP Logging Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
- RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage — https://tools.ietf.org/html/rfc6750
- MDN: Authorization header — https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
- Semgrep rule: detect-secrets-in-body — https://semgrep.dev/r?q=javascript.lang.security.detect-secrets-in-body
- GitHub PR: fix: fix security issue in Chatbot.jsx