Back to Blog
critical SEVERITY5 min read

How API Key Exposure in URL Query Parameters Happens in Node.js and How to Fix It

A critical security vulnerability was discovered in the `lib/crux.js` file where the CrUX API key was being transmitted as a URL query parameter instead of using secure HTTP headers. This exposed the API key in server logs, proxy logs, browser history, and network monitoring tools. The fix moves the API key to the `X-Goog-Api-Key` header, preventing credential leakage across logging systems.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 2, 2026•Reviewed September 2, 2026

Answer Summary

This vulnerability involves API key exposure through URL query parameters in a Node.js library (CWE-598). The `postJson` function in `lib/crux.js` was appending the CrUX API key directly to the URL as `?key=${apiKey}`, causing credentials to appear in server logs, proxy logs, and browser history. The fix moves the API key from the URL to the `X-Goog-Api-Key` HTTP header, which is the secure, recommended method for Google API authentication.

Vulnerability at a Glance

cweCWE-598
fixMove API key from URL query string to X-Goog-Api-Key header
riskAPI key theft through log file access, enabling unauthorized API usage
languageJavaScript/Node.js
root causeAPI key passed as URL query parameter instead of HTTP header
vulnerabilitySensitive Data Exposure via URL Query Parameter

Introduction

The lib/crux.js file handles communication with Google's Chrome User Experience Report (CrUX) API, fetching real-world performance data for web pages. However, a critical flaw in the postJson function at line 126 created a significant security risk: the API key was being transmitted as a URL query parameter, exposing it to anyone with access to logs or network traffic.

This vulnerability is particularly concerning because this is a Node.js library—meaning every downstream consumer who uses this package inherits this security flaw. The exposed API key could be extracted from server logs, proxy logs, or network captures, allowing attackers to make unauthorized API calls at the victim's expense.

The Vulnerability Explained

When making HTTP requests, there are two common ways to pass authentication credentials: in the URL as query parameters, or in HTTP headers. The vulnerable code chose the former approach:

const res = await fetch(`${endpoint}?key=${encodeURIComponent(apiKey)}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
});

While encodeURIComponent() properly handles special characters in the API key, it doesn't address the fundamental problem: the API key is now part of the URL itself.

Why This Is Dangerous

URLs are logged everywhere:

  1. Web server access logs: Most servers log every request URL by default
  2. Proxy and CDN logs: Corporate proxies, load balancers, and CDNs capture full URLs
  3. Browser history: If this code runs client-side, the URL appears in history
  4. Network monitoring tools: Security appliances and debugging tools capture URLs
  5. Referrer headers: The URL (including the key) may leak to third parties via the Referer header

Attack Scenario

Imagine a development team using this library in their performance monitoring pipeline. Their infrastructure includes:

  1. A corporate proxy that logs all outbound requests
  2. A SIEM system that aggregates these logs for security analysis
  3. A junior developer with read access to log files for debugging

The junior developer—or worse, an attacker who compromises their account—can simply grep through the logs:

grep "key=" /var/log/proxy/access.log | cut -d'=' -f2 | cut -d'&' -f1

Within seconds, they have valid CrUX API keys that can be used for unauthorized API calls, potentially exhausting quotas or incurring charges on the victim's Google Cloud account.

The Fix

The fix is elegantly simple: move the API key from the URL to an HTTP header. Here's the before and after:

Before (Vulnerable)

const res = await fetch(`${endpoint}?key=${encodeURIComponent(apiKey)}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
});

After (Secure)

const res = await fetch(endpoint, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Goog-Api-Key": apiKey },
    body: JSON.stringify(body),
});

Why This Works

The X-Goog-Api-Key header is Google's recommended method for API authentication. By moving the key to a header:

  1. Server logs don't capture it: Standard access logs only record URLs, not request headers
  2. Proxy logs are safer: Most proxies don't log full headers by default
  3. Browser history is clean: The URL no longer contains sensitive data
  4. Referrer leaks are prevented: Headers aren't included in Referer

The change is minimal—just two lines modified—but the security improvement is substantial. The API key is now transmitted securely while maintaining full compatibility with Google's CrUX API.

Key Takeaways

  • The postJson function in lib/crux.js was leaking API keys through URL query parameters—a pattern that affects all downstream consumers of this Node.js library
  • encodeURIComponent() doesn't protect secrets—it only handles URL encoding, not confidentiality
  • Google APIs support header-based authentication via X-Goog-Api-Key—always prefer this over URL parameters
  • A two-line change eliminated the vulnerability—security fixes don't have to be complex
  • Library authors have amplified responsibility—vulnerabilities in shared code affect every project that depends on it

How Orbis AppSec Detected This

  • Source: The apiKey parameter passed to the postJson function in lib/crux.js
  • Sink: URL string concatenation at line 126: `${endpoint}?key=${encodeURIComponent(apiKey)}`
  • Missing control: No mechanism to prevent the API key from being included in the URL; header-based authentication was not implemented
  • CWE: CWE-598 (Use of GET Request Method With Sensitive Query Strings)
  • Fix: Moved the API key from the URL query parameter to the X-Goog-Api-Key HTTP header

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 how a seemingly minor implementation choice—passing an API key in a URL versus a header—can create significant security exposure. The fix required changing just two lines of code, but the impact is substantial: API keys are no longer leaked across logging infrastructure, protecting both the library maintainers and every downstream user.

When working with APIs, always ask: "Where will this credential appear?" If the answer includes "URLs," "logs," or "browser history," it's time to refactor. Modern APIs universally support header-based authentication for exactly this reason.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #8

Related Articles

critical

ARTIFICIAL_ANALYSIS_API_KEY: Unvalidated os.getenv in Model Sync

A model-catalogue sync utility read the `ARTIFICIAL_ANALYSIS_API_KEY` environment variable with a bare `os.getenv()` call and passed whatever it found straight into `fetch_intelligence_from_api()`. Any malformed value — a trailing newline from an `--env-file`, a whitespace-only placeholder, or a pasted `Bearer …` prefix — was treated as a valid credential and forwarded into an outbound HTTP request, where it could surface in CI error output. The fix normalizes the value, validates it against a s

critical

redmine_drawio View Hook Inlines Base64 Redmine API Keys

The Redmine drawio plugin's body-bottom view listener embedded the logged-in user's Redmine REST API key into client-side JavaScript on every wiki and issue page, "protected" only by Base64 encoding of the reversed string. Any script on the page — or anyone with browser developer tools, a cached copy of the HTML, or a proxy log — could decode it in one line and act as that user through the Redmine REST API. The fix removes the embedded credential from the rendered page entirely; the plugin's dia

critical

Yandex Translate API Key Leaked via URL Query Parameter

The `translateYandex()` helper built its request URL by interpolating the caller-supplied API key directly into the query string, meaning every call leaked the credential into server access logs, proxy logs, and any Referer header sent by intermediaries. The fix switches the request from a GET with the key in the URL to a POST with the key in the request body via `URLSearchParams`, removing the credential from any URL-logging surface entirely.

critical

Google Generative AI Keys Exposed in Client-Side Fetch URLs

A client-side AI model discovery utility was embedding Google Generative AI API keys directly in fetch request URLs, making them visible to any user inspecting network traffic or browser DevTools. The fix moves the key from the URL query parameter to a secure HTTP header, eliminating exposure while maintaining API authentication.

critical

How credential leakage through console logging happens in JavaScript browser extensions and how to fix it

A browser extension's `src/background/credentials.js` printed the full Strava authentication cookie string — including a signed JWT and CloudFront-Signature values — straight into the extension console via `console.debug`. Anyone who could open DevTools on the background page (or any tooling that scraped the console) could copy a live session and impersonate the user. The fix replaces the credential payload in both log statements with `Boolean(credentials)` and strips a realistic-looking JWT out

critical

How Hardcoded Secrets Compromise Authentication in JavaScript and How to Fix It

A critical vulnerability in `Tool/QuantumultX/Rewrite/RRSP.js` exposed hardcoded API authentication credentials—a TOKEN and UMID device identifier—directly in source code. Anyone with repository access could extract these credentials to impersonate the legitimate user and gain full account access to the RRTV API service. The fix replaced hardcoded secrets with empty placeholders, forcing users to manually configure credentials through secure channels.