TITLE: Yandex Translate API Key Leaked via URL Query Parameter
SEO_TITLE: Yandex API Key in URL: CWE-798 POST Fix
SEO_DESCRIPTION: translateYandex() sent the Yandex Translate API key as a URL query string, leaking it via logs and Referer headers; CWE-798 fixed by moving it to a POST body.
SUMMARY: 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.
INTRODUCTION: The translateYandex(text, targetLang, apiKey) function in the translation helper module built its outbound request like this: https://translate.yandex.net/api/v1.5/tr.json/translate?key=${apiKey}&text=...&lang=en-${targetLang}, then fired it with a plain axios({url, timeout: 100000}) GET call. Nothing here hardcodes a secret in source — the key is passed in as a parameter — but the transmission mechanism itself is the vulnerability. Any value placed in a URL query string is liable to be written to web server access logs, forwarding proxy logs, browser history (if the URL is ever rendered client-side), and the Referer header of any subsequent request triggered from a page displaying that URL. For an application that calls a third-party translation API on behalf of many requests, this means the API key shows up, in plaintext, in every log line that records an outbound HTTP request URL — a durable, easily overlooked leak.
Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) |
| Ecosystem | npm |
| CVE / GHSA | not assigned |
| CWE | CWE-798 (Use of Hard-coded Credentials) |
The Vulnerability Explained
Here is the vulnerable request construction:
const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${apiKey}&text=${encodeURIComponent(text)}&lang=en-${targetLang}`;
const response = await axios({url, timeout: 100000});
The problem is the key=${apiKey} segment. Because apiKey is embedded directly in the URL rather than sent in a request body or header, it travels through every layer that touches the URL string:
- Access logs on any reverse proxy, load balancer, or CDN sitting in front of the outbound call (or logging the outbound call) will record the full URL, key included.
- Referer headers: if this URL were ever surfaced in a browser context or forwarded by an intermediate service, the
Refererheader on the next hop would carry the key to a third party. - Application-level logging: it's common practice to log
axiosrequest configs or errors, including theurlfield, for debugging — which means the key ends up in application logs by default, not by mistake. - Browser history / dev tools: if this call is ever proxied through a client-side network layer during development or debugging, the key sits in the network tab and history.
An attacker doesn't need to compromise the translation service to exploit this — they just need read access to any of those log stores, which is a far more common foothold than direct code execution. Once obtained, the Yandex API key can be reused to make translation calls billed to the victim's account, potentially exhausting quota or incurring cost, and depending on the key's scope, could expose usage patterns tied to translated text.
The Fix
The fix moves the API key out of the URL and into a POST request body, using URLSearchParams to encode the parameters:
const url = "https://translate.yandex.net/api/v1.5/tr.json/translate";
const body = new URLSearchParams({key: apiKey, text, lang: `en-${targetLang}`});
const response = await axios.post(url, body, {timeout: 100000});
Two things changed, and both matter:
axios({url})→axios.post(url, body, ...): switching HTTP methods means the URL that gets logged by any proxy, load balancer, oraxiosinterceptor is now justhttps://translate.yandex.net/api/v1.5/tr.json/translate— no key, no text, no target language. The sensitive parameter no longer exists in any URL-based log surface.- Query string →
URLSearchParamsbody: the key, text, and language are now form-encoded in the POST body, which is not captured by standard access-log configurations and is not reflected inRefererheaders, since those only ever carry the URL of the referring page/request, not its body.
The included regression test drives this home by asserting that whatever URL is actually sent to axios.request/axios.post never contains key=${apiKey} for a range of key values, including one with special characters — confirming the credential genuinely never appears in the URL under adversarial input.
Key Takeaways
- Passing any credential as a URL query parameter is a leak vector even if the credential itself isn't hardcoded —
translateYandex()'sapiKeyargument was always caller-supplied, yet the URL construction on line 30 still exposed it. axios({url})with a GET-style query string logs identically to any other outbound URL; if your logging captures request URLs (a very common default), any secret in that URL is now durably persisted in log storage.- Moving from
?key=${apiKey}&text=...to a POST body viaURLSearchParams({key: apiKey, text, lang})removes the secret from every URL-based log, proxy trace, andRefererheader in one change. - When wrapping a third-party API that requires a key, check whether that API supports the key in a header or POST body before defaulting to whatever query-string example is in its documentation.
How Orbis AppSec Detected This
- Source: the
apiKeyparameter passed intotranslateYandex(text, targetLang, apiKey) - Sink: the outbound
axios({url})GET request whoseurlstring embeddedkey=${apiKey} - Missing control: no separation between the credential and the URL — the key was interpolated into a query string transmitted and logged as part of the request URL
- CWE: CWE-798 (Use of Hard-coded Credentials) — flagged for insecure credential handling in transit
- Fix: the request was rewritten to send the key in a POST body via
URLSearchParamsinstead of the URL query string
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 finding is a reminder that credential exposure isn't limited to hardcoded values in source — how a credential is transmitted matters just as much as where it's stored. translateYandex() never hardcoded the Yandex API key, but by placing it in the URL query string of an axios GET request, it guaranteed the key would be written into every log surface that records request URLs. Switching to axios.post() with the key encoded in a URLSearchParams body closes that leak without changing the function's public signature — callers of translateYandex(text, targetLang, apiKey) see no difference, but the key no longer travels anywhere it can be silently captured.
FAQ:
Q: Does the fix to translateYandex() change its function signature or how callers invoke it?
A: No. translateYandex(text, targetLang, apiKey) keeps the same signature; only the internal HTTP call changed from a GET with a query string to a axios.post() call with a URLSearchParams body.
Q: Why does moving the API key from the URL to a POST body via URLSearchParams actually stop the leak?
A: Standard access logs, proxies, and Referer headers capture the request URL, not the request body, so once key moves out of the query string in axios({url}) and into the POST body, it no longer appears in those logging surfaces.
Q: Was the Yandex API key ever hardcoded in the source for translateYandex()?
A: No — apiKey was always passed in as a function parameter; the vulnerability was the insecure query-string transmission (?key=${apiKey}), not a hardcoded default value.
TAGS: hardcoded-secrets, credential-exposure, nodejs, axios, api-security, information-disclosure