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

medium

How Insufficient Password Hashing Cost Factor Happens in Node.js and How to Fix It

A bcrypt password hashing implementation in the User.js model was using a cost factor of 10, which falls below OWASP's 2024 recommendation of 12 for applications handling sensitive data. This fix upgrades the salt rounds from 10 to 12, increasing the computational work required to crack passwords by approximately 4x, significantly improving protection against brute-force attacks on this e-commerce platform.

high

How Arbitrary Code Execution via Template Imports Happens in JavaScript (lodash) and How to Fix It

A high-severity arbitrary code execution vulnerability (CVE-2026-4800) was discovered in lodash's template function, specifically in how it handles the `imports` option with untrusted input. The fix upgrades lodash from version 4.17.21 to 4.18.0 in the project's `package.json` and `yarn.lock`, eliminating the attack surface where crafted template imports could execute arbitrary code on the server.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js IP address parsing and how to fix it

A critical SSRF vulnerability (CVE-2026-69192) was discovered in the ip-address npm package version 10.2.0, which could allow attackers to bypass IP address validation and access internal services. The fix upgrades the dependency to version 10.3.1, which properly handles edge cases in IP address parsing that previously allowed trust-boundary bypasses.

critical

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.

high

How Authentication Bypass happens in Next.js App Router with Turbopack and how to fix it

A critical authentication bypass vulnerability (CVE-2026-64642) was discovered in Next.js versions prior to 16.2.11, specifically affecting App Router applications using Turbopack with a single locale configuration. This vulnerability allowed attackers to bypass middleware and proxy protections, potentially gaining unauthorized access to protected routes and resources that should have been secured by authentication checks.

critical

How SQL Injection Happens in CSV-to-SQL Converters and How to Fix It

A critical SQL injection vulnerability was discovered in the `csv2sql()` function in `src/data/converter/csv.js`, where CSV data and table names were directly interpolated into SQL INSERT statements without sanitization. The fix implements input validation through identifier sanitization and proper value escaping, eliminating the attack surface while preserving legitimate functionality.