← Blog

Is Your HTTP Client Leaking Your API Keys?

Your application calls an API with a secret key in a header - Authorization, or something custom like X-API-Key. You trust that key only goes to the host you expect. Then that host, one day, responds with a 302 redirect to a different address. Your HTTP library, as it does by default, follows the redirect. Million-dollar question: does your secret key get sent to the new destination too?

The correct answer would be "no". It is also the answer most developers would give without thinking twice. But in too many of the world's most used HTTP libraries the real answer is "it depends" - and "it depends", when the subject is a credential, is already a vulnerability. This article explains why it happens, gives you a thirty-second test to find out whether you are exposed, and shows you how to close the hole without waiting for someone else to do it for you.

What should happen (and what actually happens)

The correct behavior rests on a precise concept: the origin. On the web an origin is not the domain, it is the triple (scheme, host, port). https://api.example.com and https://api.example.com:8443 are two different origins, exactly as example.com and evil.com are. The rule is simple: credentials belong to the origin you addressed them to, and should never cross an origin change without an explicit decision from you.

Almost every library implements the easy version of this rule: if the redirect points to a different domain, strip the sensitive headers. The problem is which headers they consider sensitive. Almost always the list is hardcoded and very short: Authorization, Cookie, Proxy-Authorization. That's it. If your application authenticates with a custom header - and very many do: X-API-Key, X-Auth-Token, Api-Key, Token - the library has no way of knowing that header is a credential. So it treats it as just another header and carries it along to the redirect target.

Your app X-API-Key api.example.com 302 redirect attacker.com your key leaks here
The credential travels past the origin boundary, to a host that should never have seen it.

Test it yourself in thirty seconds

You don't have to take my word for it. Open a terminal and have curl follow a cross-host redirect with a custom header, sending it to an endpoint that echoes back the headers it received:

curl -sSL -H "X-API-Key: do-not-leak-me"   "https://httpbin.org/redirect-to?url=https://postman-echo.com/get&status_code=302"

The first URL is on httpbin.org, but it responds with a 302 to postman-echo.com - a different host. In the final response, look at the echoed headers object: there is your X-API-Key, having happily travelled from one origin to another. Your application's HTTP client, in the vast majority of cases, behaves exactly the same way. What you just saw with curl is the same mechanism that leaks keys out of real applications.

The three blind spots

To sum up where it breaks, in order of frequency:

  • Custom headers not covered. The protection strips Authorization but ignores X-API-Key and friends. This is the most common and most underestimated case, because the developer feels safe precisely because they use "their own" header.
  • Port confusion. A redirect from http://host:8080 to https://host:9443 is an origin change (the port differs). Several libraries compared only the hostname, concluded "same host" and kept the credentials. The same applies to the http-to-https upgrade treated as "safe" regardless.
  • Redirect chains. The single base case gets handled, but a chain - A redirects to B which redirects to C - or a redirect through an intermediary, can bring back a header that was stripped on the first hop.

It is not an isolated case. It is everywhere.

I know it is everywhere because I went looking for it systematically, one library after another. The same bug class, confirmed across six widely used HTTP libraries in the Node.js and Go ecosystems: undici (the client behind Node.js's global fetch), node-fetch, follow-redirects (axios's redirect dependency, present across half the npm ecosystem), go-resty, req, and gorequest - plus a published CVE on one of them. These are not abandoned projects: they are components running in production inside millions of applications, with millions of downloads a week. The point is not that the authors are careless - it is that this trust boundary is so easy to take for granted that everyone trips over it. Nobody tests their HTTP library, because "it works". And that is exactly where the worst bugs hide: in the pieces nobody looks at anymore.

When it becomes a real attack

"Sure, but you need an attacker who controls a redirect": true, and less rare than it sounds. Three concrete scenarios:

  • Webhooks and configurable URLs. Does your app let the customer set a callback URL? If that request travels with a credential and follows redirects, the customer (or whoever compromised their account) can point it at a server that answers 302 and harvests the header.
  • Third-party integrations. You call an external service with your key. That service, compromised or malicious, responds with a redirect to a host it controls. Your key leaves on its own.
  • Cloud and service-to-service. Between microservices, redirects are routine and credentials (tokens, internal API keys) flow constantly. A single compromised service that knows how to answer with a redirect becomes a collection point.

In all three the impact is the same: exfiltration of valid credentials, in the clear, to an origin that should never have seen them. From there it is account takeover, data access, lateral movement.

How to protect yourself

The important thing: don't wait for the library patch. You can close the hole at the application level today, with one guiding principle - treat every redirect on an authenticated request as a trust boundary, not a transport detail - and three tactics.

1. Handle redirects manually for authenticated calls. Disable automatic following and decide yourself whether and where to re-send the credential. In axios:

const res = await axios.get(url, {
  headers: { "X-API-Key": secret },
  maxRedirects: 0,                 // do not follow automatically
  validateStatus: (s) => s < 400, // handle 3xx yourself
});

2. Strip sensitive headers on every hop. In Go with net/http you have CheckRedirect, which lets you mutate the request on each hop:

client := &http.Client{
  CheckRedirect: func(req *http.Request, via []*http.Request) error {
    req.Header.Del("X-API-Key")
    req.Header.Del("Authorization")
    return nil
  },
}

3. Pin the destination. Where the library allows it, extend the strip list with your custom headers (follow-redirects, for instance, added a dedicated option after the reports), and where you can, accept redirects only to an allowlist of expected hosts, instead of blindly following any Location.

The checklist, in short

  • Do you send credentials in custom headers (not just Authorization)? You are in the at-risk category.
  • Does your client follow redirects by default? Almost certainly yes.
  • Can any endpoint you call be made to respond with a redirect? Webhooks, configurable URLs, integrations: treat them as untrusted.
  • For authenticated outbound calls: manual redirects, sensitive headers stripped on every hop, destinations on an allowlist.

The bigger lesson

Your application's security does not live only in the code you write. It also lives in the dozens of dependencies your app drags along, and in the assumptions those dependencies make on your behalf - assumptions you never read, about behaviors you take for granted. One of those assumptions, in a component you use every day without thinking, can take your most valuable key and hand it to a complete stranger. The defender's job, today, is to know which assumptions you are inheriting - and to trust none of them when a credential is on the line.

Need an expert opinion?

If you want to dive deeper into this topic or need specialized consulting, let us talk.

Let's talk
Is Your HTTP Client Leaking Your API Keys? - Dennis Sepede