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.

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #79

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.