npm · @microsoft/kiota-http-fetchlibrary
@microsoft/kiota-http-fetchlibrary: Bearer token and Cookie leak across origin on redirect due to case-mismatched scrub in fetchRequestAdapter
@microsoft/kiota-http-fetchlibrary's RedirectHandler is documented as stripping Authorization and Cookie from cross-origin redirect targets, but the default scrubSensitiveHeaders callback in RedirectHandlerOptions uses case-sensitive property deletion (delete headers.Authorization, delete headers.Cookie) on a headers object that FetchRequestAdapter.getRequestFromRequestInformation has already lower-cased. The delete therefore targets keys that do not exist, the scrub is a no-op, and any Bearer token or Cookie attached by a kiota-generated SDK is forwarded to an attacker-controlled host across a 30x redirect.
This is reachable in the default middleware chain (MiddlewareFactory.getDefaultMiddlewares) with no custom configuration, and applies to every kiota-generated TypeScript SDK that uses BaseBearerTokenAuthenticationProvider or any other authentication provider that sets the Authorization request header.
@microsoft/kiota-http-fetchlibrary >= 1.0.0-preview.97 (the release that introduced the defaultScrubSensitiveHeaders callback, commit 74886cc4, tagged 2026-02-27) up to and including 1.0.0-preview.101 (latest at filing). The bug was verified end-to-end against the version published on npm: 1.0.0-preview.100.
The case-mismatch primitive (lowercasing in the request adapter) predates the scrub itself — FetchRequestAdapter.getRequestFromRequestInformation has lower-cased header keys via toLocaleLowerCase() since commit d612bac2 (2022-12-09). When the scrub was added in 2026-02 it inherited the mismatch.
Authorization: Bearer <token> header issued by the auth provider is sent in cleartext to the redirect target. The redirect target can be controlled by:Cookie header, the same primitive forwards it to the redirect target.The two pieces that combine into the bug.
1. Headers are lower-cased on the way out of the request adapter.
packages/http/fetch/src/fetchRequestAdapter.ts:529-532:
const headers: Record<string, string> | undefined = {};
requestInfo.headers?.forEach((_, key) => {
headers[key.toString().toLocaleLowerCase()] = this.foldHeaderValue(requestInfo.headers.tryGetValue(key));
});
The headers object that flows into the middleware pipeline as fetchRequestInit.headers has every key lower-cased. So Authorization becomes authorization, Cookie becomes cookie.
2. The default redirect scrub deletes case-sensitive property names.
packages/http/fetch/src/middlewares/options/redirectHandlerOptions.ts:67-82:
private static readonly defaultScrubSensitiveHeaders: ScrubSensitiveHeaders = (headers: Record<string, string>, originalUrl: string, newUrl: string) => {
if (!headers || !originalUrl || !newUrl) {
return;
}
try {
const originalUri = new URL(originalUrl);
const newUri = new URL(newUrl);
const isDifferentHostOrScheme = originalUri.host.toLowerCase() !== newUri.host.toLowerCase() || originalUri.protocol.toLowerCase() !== newUri.protocol.toLowerCase();
if (isDifferentHostOrScheme) {
delete headers.Authorization;
delete headers.Cookie;
}
} catch {
return;
}
};
delete headers.Authorization is sugar for delete headers["Authorization"]. JavaScript object property names are case-sensitive. The headers object's actual key is "authorization" (lower-case). The delete removes nothing.
3. The redirect handler invokes the scrub on the lower-cased object.
packages/http/fetch/src/middlewares/redirectHandler.ts:133-136:
if (fetchRequestInit.headers) {
currentOptions.scrubSensitiveHeaders(fetchRequestInit.headers as Record<string, string>, url, newUrl);
}
The redirect handler then issues a new fetch with the unchanged fetchRequestInit.headers (still containing authorization) to newUrl (the attacker-controlled host).
FetchRequestAdapter.send calls authenticationProvider.authenticateRequest(requestInfo). BaseBearerTokenAuthenticationProvider adds Authorization: Bearer <token> to requestInfo.headers (packages/abstractions/src/authentication/baseBearerTokenAuthenticationProvider.ts:34).FetchRequestAdapter.getRequestFromRequestInformation builds the RequestInit object, lower-casing every header key. The output headers map contains key "authorization".RetryHandler then RedirectHandler. RedirectHandler.execute sets redirect = "manual" so the underlying fetch does not auto-follow.authorization: Bearer <token>.302 Location: https://attacker.example/loot.RedirectHandler.executeWithRedirect sees the 302, parses the Location, computes newUrl, and calls currentOptions.scrubSensitiveHeaders(headers, url, newUrl).defaultScrubSensitiveHeaders correctly observes originalUri.host !== newUri.host, enters the if (isDifferentHostOrScheme) branch, and runs delete headers.Authorization. The headers object's key is authorization. The delete is a no-op.executeWithRedirect recurses with url = newUrl and the unchanged headers. A second fetch goes out to the attacker host carrying authorization: Bearer <token> and cookie: <session>.End-to-end PoC against @microsoft/kiota-http-fetchlibrary@1.0.0-preview.100 and @microsoft/kiota-abstractions@1.0.0-preview.99 installed from npm with npm install. Two local HTTP listeners simulate the victim host (port 7771) and the attacker host (port 7772). The attacker listener captures the full set of request headers it observes.
package.json:
{
"name": "kiota-bearer-leak-poc",
"version": "0.0.1",
"private": true,
"type": "module",
"dependencies": {
"@microsoft/kiota-abstractions": "^1.0.0-preview.99",
"@microsoft/kiota-http-fetchlibrary": "^1.0.0-preview.99"
}
}
poc.mjs:
import http from "node:http";
import {
BaseBearerTokenAuthenticationProvider,
RequestInformation,
HttpMethod,
} from "@microsoft/kio
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.