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
-
Configuration manipulation: An attacker with access to
chrome.storage.local(via a compromised extension or XSS in the extension's context) changes the storedbaseUrlfromhttps://api.openai.com/v1tohttp://api.openai.com/v1. -
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. -
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.
-
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
- OWASP: Transport Layer Protection Cheat Sheet
- CWE-319: Cleartext Transmission of Sensitive Information
- CWE-522: Insufficiently Protected Credentials
Key Takeaways
- The
buildModelApiRequestfunction must never sendAuthorizationheaders over HTTP to remote endpoints—this is now enforced with anINSECURE_URLerror code - Loopback validation requires exact hostname matching, not prefix matching—
127.0.0.1.evil.exampleis 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.xwould 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
isLoopbackUrlfunction should be reused anywhere the extension makes authenticated requests, not just inbuildModelApiRequest
How Orbis AppSec Detected This
- Source: API key loaded from
chrome.storage.localinto theconfig.apiKeyproperty - Sink:
Authorizationheader in the fetch request constructed bybuildModelApiRequestinutils/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
isLoopbackUrlvalidation and HTTPS enforcement that throwsExtensionError('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
- CWE-319: Cleartext Transmission of Sensitive Information
- CWE-522: Insufficiently Protected Credentials
- OWASP Transport Layer Protection Cheat Sheet
- OWASP API Security Top 10 - API2:2023 Broken Authentication
- MDN Web Docs: URL API
- Semgrep Rules: Insecure Transport
- fix: the buildmodelapirequest function constructs ap... in common.js