Back to Blog
critical SEVERITY7 min read

How Insecure API Key Transmission Happens in JavaScript Browser Extensions and How to Fix It

A critical vulnerability in `utils/common.js` allowed API keys to be transmitted over unencrypted HTTP connections to remote servers, exposing them to network interception. The `buildModelApiRequest` function at line 490 constructed API requests without validating the transport protocol, enabling man-in-the-middle attacks. The fix enforces HTTPS for all remote API endpoints while preserving HTTP access for local development servers on loopback addresses.

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

Answer Summary

This is a credential exposure vulnerability (CWE-319) in a JavaScript browser extension where the `buildModelApiRequest` function in `utils/common.js` transmits API keys in Authorization headers over potentially insecure HTTP connections. The fix introduces URL protocol validation that enforces HTTPS for all remote endpoints, only permitting HTTP for loopback addresses (localhost, 127.0.0.1, [::1]), and throws an `ExtensionError` with code `INSECURE_URL` when insecure remote URLs are detected.

Vulnerability at a Glance

cweCWE-319
fixEnforce HTTPS for remote URLs; allow HTTP only for verified loopback addresses
riskAPI keys intercepted via network traffic on insecure HTTP connections
languageJavaScript
root causeNo protocol validation in buildModelApiRequest before sending credentials
vulnerabilityCleartext Transmission of Sensitive Information

Introduction

The utils/common.js file in this browser extension handles the construction of API requests to external AI model providers. At line 490, the buildModelApiRequest function takes a configuration object containing an API key and a base URL, then builds a fetch request with the key placed in the Authorization header. The critical flaw? There was absolutely no validation that the target URL used HTTPS—meaning API keys could be sent in plaintext over HTTP to any remote server, fully visible to anyone intercepting network traffic.

This isn't a theoretical concern. Browser extensions routinely allow users to configure custom API endpoints (for self-hosted models like Ollama or alternative providers). A user misconfiguring their endpoint as http://api.example.com instead of https://api.example.com—or an attacker manipulating stored configuration—would cause every API key to be transmitted without encryption.

The Vulnerability Explained

How the Code Worked Before

The buildModelApiRequest function accepted a config object with properties including apiKey, baseUrl, and apiFormat. It constructed a request like this:

function buildModelApiRequest(config, prompt) {
  const url = config.baseUrl;
  const fetchOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${config.apiKey}`
    },
    body: JSON.stringify({ model: config.modelName, messages: [{ role: 'user', content: prompt }] })
  };
  return { url, fetchOptions };
}

The function blindly trusted whatever baseUrl was provided. If a user configured http://api.openai.com/v1 (note: HTTP, not HTTPS), the API key would be placed in the Authorization header and sent over an unencrypted connection.

The Attack Scenario

  1. Configuration manipulation: An attacker with access to chrome.storage.local (via a compromised extension or XSS in the extension's context) changes the stored baseUrl from https://api.openai.com/v1 to http://api.openai.com/v1.

  2. Network interception: On any shared network (coffee shop WiFi, corporate network, compromised router), an attacker runs a simple packet capture. Because the request uses HTTP, the full Authorization: Bearer sk-abc123... header is visible in plaintext.

  3. Credential theft: The attacker extracts the API key and uses it to make unlimited API calls at the victim's expense, or accesses any data associated with that API account.

  4. Hostname spoofing bypass: Even more dangerously, an attacker could set the base URL to something like http://127.0.0.1.evil.example—a domain that looks like a loopback address but actually resolves to an attacker-controlled server. Without proper hostname validation, the extension would happily send credentials to this malicious endpoint.

Why This Is Critical

  • API keys are high-value credentials: OpenAI, Anthropic, and similar API keys often have billing attached with no per-request spending limits
  • Browser extensions have broad access: The extension operates across all tabs, making credential theft particularly impactful
  • Users configure endpoints manually: The self-hosted model use case (Ollama, LM Studio) means HTTP URLs are common in user configurations
  • No user-visible warning: The extension gave no indication that credentials were being transmitted insecurely

The Fix

The fix introduces two key security mechanisms: a isLoopbackUrl validation function and HTTPS enforcement in buildModelApiRequest.

New Security Functions

function isLoopbackUrl(urlString) {
  try {
    const url = new URL(urlString);
    const hostname = url.hostname;
    // Only true loopback addresses are allowed over HTTP
    return hostname === 'localhost' ||
           hostname === '127.0.0.1' ||
           /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) ||
           hostname === '[::1]';
  } catch {
    return false;
  }
}

HTTPS Enforcement in buildModelApiRequest

function buildModelApiRequest(config, prompt) {
  const url = config.baseUrl;

  // NEW: Enforce HTTPS for all remote URLs
  const parsedUrl = new URL(url);
  if (parsedUrl.protocol === 'http:' && !isLoopbackUrl(url)) {
    throw new ExtensionError('INSECURE_URL', 
      'API requests with credentials require HTTPS for remote endpoints');
  }

  const fetchOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${config.apiKey}`
    },
    body: JSON.stringify({ model: config.modelName, messages: [{ role: 'user', content: prompt }] })
  };
  return { url, fetchOptions };
}

What Changed and Why

Before After
Any URL accepted without validation URL protocol checked before attaching credentials
HTTP to remote servers silently allowed ExtensionError with code INSECURE_URL thrown for remote HTTP
No loopback detection Strict loopback validation for localhost, 127.x.x.x, [::1]
Hostname spoofing possible Regex validates actual IP octets, blocking 127.0.0.1.evil.example

Critical Design Decision: Loopback Exception

The fix correctly preserves HTTP access for legitimate local development scenarios:

// These are ALLOWED (local model servers)
assertAllowed('http://localhost:11434/v1');      // Ollama default
assertAllowed('http://127.0.0.1:8080/v1');      // LM Studio
assertAllowed('http://[::1]:8080/v1');           // IPv6 loopback

// These are BLOCKED (remote or spoofed)
assertInsecureUrl('http://api.example.com');              // Remote HTTP
assertInsecureUrl('http://127.0.0.1.evil.example');      // Spoofed loopback
assertInsecureUrl('http://192.168.1.100:8080/v1');       // LAN (not loopback)
assertInsecureUrl('http://10.0.0.1:11434/v1');           // Private network

Notice that RFC-1918 private addresses (192.168.x.x, 10.x.x.x, 172.16.x.x) are intentionally blocked even though they're "local network." This is a deliberate security policy: only the machine's own loopback interface is trusted for unencrypted credential transmission.

Hostname Spoofing Prevention

The test suite explicitly guards against bypass attempts:

// Hostname-prefix bypass regression — must be blocked
assertInsecureUrl('http://10.evil.example');
assertInsecureUrl('http://172.16.evil.example');
assertInsecureUrl('http://192.168.evil.example');
assertInsecureUrl('http://127.0.0.1.evil.example');

A naive implementation might check hostname.startsWith('127.') which would be bypassed by 127.0.0.1.evil.example. The regex /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ensures the entire hostname matches the loopback pattern with no trailing characters.

Prevention & Best Practices

1. Always Validate Transport Security Before Attaching Credentials

// BAD: Blind trust
headers['Authorization'] = `Bearer ${apiKey}`;

// GOOD: Validate first
if (!isSecureTransport(url)) {
  throw new Error('Credentials require secure transport');
}
headers['Authorization'] = `Bearer ${apiKey}`;

2. Use Allowlists, Not Blocklists

The fix uses an allowlist approach—only explicitly verified loopback addresses are permitted over HTTP. Everything else must use HTTPS. This is far more secure than trying to block known-bad patterns.

3. Defense in Depth for Stored Credentials

The PR description notes that PBKDF2 is available in the Rust dependencies but unused for encrypting stored tokens. A complete fix should also:
- Encrypt API keys at rest using PBKDF2-derived keys
- Use the OS keychain where available (via Tauri's native APIs)
- Implement credential rotation reminders

4. Content Security Policy

Browser extensions should set a restrictive CSP that limits connect-src to HTTPS origins only, providing a browser-level enforcement layer.

5. Relevant Standards

Key Takeaways

  • The buildModelApiRequest function must never send Authorization headers over HTTP to remote endpoints—this is now enforced with an INSECURE_URL error code
  • Loopback validation requires exact hostname matching, not prefix matching—127.0.0.1.evil.example is a real bypass vector that the regex /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ prevents
  • RFC-1918 private addresses (LAN) are NOT equivalent to loopback—trusting 192.168.x.x would allow credential theft by any device on the same network
  • User-configurable API endpoints are a credential exposure vector—any time users can set a URL that will receive their credentials, protocol validation is mandatory
  • The isLoopbackUrl function should be reused anywhere the extension makes authenticated requests, not just in buildModelApiRequest

How Orbis AppSec Detected This

  • Source: API key loaded from chrome.storage.local into the config.apiKey property
  • Sink: Authorization header in the fetch request constructed by buildModelApiRequest in utils/common.js:490
  • Missing control: No URL protocol validation before attaching credentials to the request—HTTP URLs to remote servers were accepted without restriction
  • CWE: CWE-319 (Cleartext Transmission of Sensitive Information)
  • Fix: Added isLoopbackUrl validation and HTTPS enforcement that throws ExtensionError('INSECURE_URL') for any non-loopback HTTP endpoint

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 a subtle but critical gap in browser extension security: the assumption that configured URLs will always use HTTPS. In practice, users configuring local AI model servers (Ollama, LM Studio) routinely use HTTP, and a single character difference between http:// and https:// determines whether API keys worth hundreds or thousands of dollars are transmitted securely or broadcast to anyone listening on the network.

The fix is elegant in its approach—enforcing HTTPS universally while carving out a narrow, well-validated exception for true loopback addresses. The comprehensive test suite guards against hostname spoofing bypasses that would defeat simpler implementations. For any developer building browser extensions or client-side applications that handle API credentials, this pattern of "validate transport before attaching credentials" should be a standard security checkpoint.

References

Frequently Asked Questions

What is cleartext transmission of sensitive information?

It occurs when sensitive data like API keys or passwords are sent over unencrypted channels (HTTP instead of HTTPS), allowing anyone monitoring network traffic to capture the credentials.

How do you prevent credential exposure in JavaScript browser extensions?

Enforce HTTPS for all remote API requests, validate URLs before attaching credentials, use the browser's built-in credential storage APIs, and never embed keys in URLs or request bodies.

What CWE is cleartext credential transmission?

CWE-319 (Cleartext Transmission of Sensitive Information) covers cases where credentials are sent without encryption, and CWE-522 (Insufficiently Protected Credentials) covers the broader storage and transmission pattern.

Is using Authorization headers enough to prevent API key exposure?

No. Authorization headers protect keys from appearing in URLs and server logs, but if the connection uses HTTP instead of HTTPS, the entire header is visible to network observers via packet sniffing or MITM attacks.

Can static analysis detect insecure API key transmission?

Yes. Static analysis tools can trace data flow from credential storage to network calls and flag cases where HTTPS is not enforced, though they may produce false positives for legitimate loopback development scenarios.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #79

Related Articles

critical

How URL Injection via Unvalidated User Input happens in Node.js and how to fix it

A critical URL injection vulnerability in the QQ info lookup feature allowed attackers to manipulate API request parameters by sending specially crafted messages. Without proper input validation, user-controlled data was directly embedded into external API URLs, potentially exposing sensitive authentication credentials (skey and pskey) to attacker-controlled servers.

high

How Denial of Service via Exponential-Time Complexity Happens in Node.js Dependencies and How to Fix It

A high-severity denial of service vulnerability (CVE-2026-14257) was discovered in the brace-expansion package within the zeroshot-oecp Docker container's dependency tree. The vulnerability allows attackers to craft malicious input patterns that trigger exponential-time processing, potentially freezing or crashing Node.js applications. This fix upgrades the nested brace-expansion dependency to version 5.0.9 using a targeted Dockerfile modification.

high

How Cache-Control Header Mishandling Happens in Node.js HTTP Clients and How to Fix It

CVE-2026-13697 is a high-severity vulnerability in undici, the popular Node.js HTTP client, where the cache interceptor fails to properly validate malformed `Cache-Control: private` directives. This could allow sensitive cached responses to be served to unauthorized users. The fix upgrades undici from 7.28.0 to 7.29.0 (and 6.27.0 to 6.28.0) across the dependency tree, including using npm overrides to patch transitive dependencies.

medium

How XML Entity Expansion Denial of Service happens in Node.js and how to fix it

A critical denial of service vulnerability (CVE-2026-33036) was discovered in fast-xml-parser versions prior to 5.5.6 and 4.5.5, allowing attackers to bypass entity expansion limits and crash Node.js applications through malicious XML payloads. This fix upgrades the dependency in the scripts directory to patched versions, protecting build pipelines and any runtime XML processing from resource exhaustion attacks.

critical

How Arbitrary Code Execution via Command Injection Happens in Node.js shell-quote and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the popular Node.js `shell-quote` package (versions prior to 1.8.4) where unescaped line terminators allowed attackers to inject and execute arbitrary shell commands. The fix upgrades `shell-quote` from version 1.8.1 to 1.8.4, which properly escapes line terminator characters (such as `\n`, `\r`, `\u2028`, and `\u2029`) before passing strings to the shell. This dependency was present in the project's `package-lock.json`

critical

How Insecure HTTPS Requests and Missing Timeouts Happen in Python and How to Fix Them

A critical security hardening issue was discovered in `scripts/maimai/songs.py` where HTTP requests were made without SSL certificate verification and timeout values. This combination creates a Man-in-the-Middle (MITM) attack vector that could allow adversaries to intercept sensitive data or inject malicious content. The fix adds explicit SSL verification enforcement and request timeouts to all HTTP calls.