Bug Report
- Import path:
github.com/Azure/azure-sdk-for-go/sdk/azcore (specifically sdk/azcore/runtime)
- SDK version:
v1.22.0 (latest published; go list -m output below). Also confirmed present on main today.
- Go version:
go version go1.26.5 windows/amd64
$ go list -m github.com/Azure/azure-sdk-for-go/sdk/azcore
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
What happened?
azcore's default http.Client is constructed without a CheckRedirect function, so Go's standard
library follows up to 10 redirects automatically. On a cross-host redirect the stdlib strips only
four headers — Authorization, Www-Authenticate, Cookie, Cookie2 — and re-sends everything
else verbatim to the new host.
That means Azure's key- and SAS-style credential headers are forwarded to the redirect target,
because none of them are named Authorization:
| Header |
Set by |
Contents |
Api-Key, Ocp-Apim-Subscription-Key |
key-credential policies / APIM-fronted services |
long-lived key |
aeg-sas-key, aeg-sas-token |
sdk/messaging/eventgrid/azeventgrid/client_custom.go |
Event Grid SAS |
x-ms-copy-source |
sdk/storage/azblob/.../zz_blob_client.go (+ appendblob, pageblob) |
source blob URL, commonly including its ?...&sig= SAS |
x-ms-copy-source-authorization |
same |
an authorization value in a header the stdlib does not strip |
Referer |
Go stdlib, automatically |
the full previous URL — leaks a SAS in the query string even when no credential header is set |
sdk/azcore/runtime/transport_default_http_client.go:
defaultHTTPClient = &http.Client{
Transport: defaultTransport,
}
No CheckRedirect, so http.Client uses defaultCheckRedirect (follow, max 10 hops). This is the
only HTTP client construction in the module, and there is no redirect handling anywhere in the repo
— grep -ri checkredirect and find -iname '*redirect*' both return nothing.
What did you expect or want to happen?
That credentials attached by the pipeline are not sent to a host other than the one they were
intended for — which is what the other five Azure SDKs already do. Each of these was read from
current main:
| SDK |
Redirect control |
| .NET |
AllowAutoRedirect = false at every transport; RedirectPolicy off by default and strips Authorization on every redirect; BearerTokenAuthenticationPolicy made host-bound in Azure.Core 1.59.0 |
| Java |
RedirectPolicy.java clears the authorization header before following, with the reason named in the comment |
| Python |
dedicated SensitiveHeaderCleanupPolicy; DEFAULT_SENSITIVE_HEADERS = {"Authorization", "x-ms-authorization-auxiliary"} |
| Rust |
.redirect(reqwest::redirect::Policy::none()) |
| JS |
allowCrossOriginRedirects = false by default |
| Go |
(none) |
Two of those seem especially relevant here:
- Python already protects a non-
Authorization credential header (x-ms-authorization-auxiliary),
so the team has already concluded that stdlib-level Authorization stripping isn't sufficient for
Azure's x-ms-* credentials. That's the same gap Go leaves open.
- Azure.Core 1.59.0 (2026-06-09) shipped a Bugs Fixed entry for this class in .NET
(Azure/azure-sdk-for-net#59606,
"[Azure.Core] Do not flow token on cross-host redirect"): RedirectPolicy stripped
Authorization, but BearerTokenAuthenticationPolicy re-added the cached token when the redirect
target was a different host.
The difference worth flagging: #59606's defect only manifested with a non-default opt-in
(HttpPipelineTransportOptions.IsClientRedirectEnabled = true). In Go, redirect-following is on by
default via the standard library and there is no policy of any kind, so the same structural gap is
reachable in the default configuration. .NET's non-bearer policies
(AzureKeyCredentialPolicy, AzureSasCredentialSynchronousPolicy) have the same shape as Go's, but
they're unreachable there precisely because auto-redirect is off.
How can we reproduce it?
Self-contained — no Azure resources and no network access. Two local servers: a "service" that answers
302 with a Location pointing at an "attacker" host, and the attacker host, which prints every
header it receives. The request goes through azcore's default transport.
Output (re-run today against azcore v1.22.0 / go1.26.5)
=== VARIANT A: azcore DEFAULT transport, cross-host 302 (http) ===
attacker host WAS contacted. Headers it received:
Api-Key: SUPER-SECRET-API-KEY-abc123 <<<<<< CREDENTIAL LEAKED
Ocp-Apim-Subscription-Key: sv=2021-08-06&sig=SUPER-SECRET-SAS-SIGNATURE <<<<<< CREDENTIAL LEAKED
Referer: http://localhost:11367/resource
X-Ms-Copy-Source: https://victim.blob.core.windows.net/c/b?sv=2021-08-06&sig=SUPER-SECRET-SAS-SIGNATURE <<<<<< CREDENTIAL LEAKED
[stripped] Authorization was NOT forwarded
=== VARIANT B: https, distinct hostnames, cross-host 302 ===
https://service.contoso.test -> 302 -> https://attacker.evil.test
(identical: Api-Key, Ocp-Apim-Subscription-Key, X-Ms-Copy-Source all forwarded)
=== VARIANT C: SAS in URL query string, cross-host 302, no credential header set ===
Referer: https://victim.blob.core.windows.test/container/blob?sv=2021-08-06&sig=SUPER-SECRET-SAS-SIGNATURE <<<<<< CREDENTIAL LEAKED
[stripped] Authorization was NOT forwarded
[stripped] Api-Key was NOT forwarded (not set in this variant)
Control: Authorization is correctly stripped in every variant, which shows the harness can
observe stripping — so the forwarded headers are a real result rather than a test artifact. In
variant C the credential headers are absent because they were not set, confirming the harness reports
only what actually arrives.
I'm happy to attach the full harness (go.mod + a single main.go) or open a PR with it as a test,
whichever is more useful to you.
Suggested fix
Set an explicit CheckRedirect on defaultHTTPClient:
defaultHTTPClient = &http.Client{
Transport: defaultTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
This matches Rust (Policy::none()) and .NET (AllowAutoRedirect = false), and the Azure REST
guidelines discourage 3xx from services anyway. If following redirects needs to be preserved, the
Python/Java shape works too: on a hop where (scheme, host, port) changes, drop the Azure credential
headers and Referer.
Happy to send a PR for either shape if you'd prefer that to an issue.
Anything we should know about your environment?
Nothing unusual — Windows, Go 1.26.5, module mode, no proxy.
Scope, stated honestly
I have not demonstrated an open redirect or Location injection on a live Azure first-party
service, and I did not test against any real Azure endpoint. What I'm reporting is that azcore
provides no protection if a cross-host redirect occurs, in a codebase where every sibling SDK
deliberately provides one. If the position is that no in-scope service can be made to emit a
cross-host 3xx, that reduces the practical impact considerably and this is just a
defence-in-depth/consistency gap — which still seems worth closing given the one-line fix.
I searched existing issues and PRs before filing: no issue or PR in this repo mentions
CheckRedirect, and #20830 ("Enhance tests for redirect policy", closed) refers to a policy that
doesn't exist in the Go SDK. Apologies if I missed a prior discussion.
Bug Report
github.com/Azure/azure-sdk-for-go/sdk/azcore(specificallysdk/azcore/runtime)v1.22.0(latest published;go list -moutput below). Also confirmed present onmaintoday.go version go1.26.5 windows/amd64What happened?
azcore's defaulthttp.Clientis constructed without aCheckRedirectfunction, so Go's standardlibrary follows up to 10 redirects automatically. On a cross-host redirect the stdlib strips only
four headers —
Authorization,Www-Authenticate,Cookie,Cookie2— and re-sends everythingelse verbatim to the new host.
That means Azure's key- and SAS-style credential headers are forwarded to the redirect target,
because none of them are named
Authorization:Api-Key,Ocp-Apim-Subscription-Keyaeg-sas-key,aeg-sas-tokensdk/messaging/eventgrid/azeventgrid/client_custom.gox-ms-copy-sourcesdk/storage/azblob/.../zz_blob_client.go(+ appendblob, pageblob)?...&sig=SASx-ms-copy-source-authorizationReferersdk/azcore/runtime/transport_default_http_client.go:No
CheckRedirect, sohttp.ClientusesdefaultCheckRedirect(follow, max 10 hops). This is theonly HTTP client construction in the module, and there is no redirect handling anywhere in the repo
—
grep -ri checkredirectandfind -iname '*redirect*'both return nothing.What did you expect or want to happen?
That credentials attached by the pipeline are not sent to a host other than the one they were
intended for — which is what the other five Azure SDKs already do. Each of these was read from
current
main:AllowAutoRedirect = falseat every transport;RedirectPolicyoff by default and stripsAuthorizationon every redirect;BearerTokenAuthenticationPolicymade host-bound in Azure.Core 1.59.0RedirectPolicy.javaclears the authorization header before following, with the reason named in the commentSensitiveHeaderCleanupPolicy;DEFAULT_SENSITIVE_HEADERS = {"Authorization", "x-ms-authorization-auxiliary"}.redirect(reqwest::redirect::Policy::none())allowCrossOriginRedirects = falseby defaultTwo of those seem especially relevant here:
Authorizationcredential header (x-ms-authorization-auxiliary),so the team has already concluded that stdlib-level
Authorizationstripping isn't sufficient forAzure's
x-ms-*credentials. That's the same gap Go leaves open.(Azure/azure-sdk-for-net#59606,
"[Azure.Core] Do not flow token on cross-host redirect"):
RedirectPolicystrippedAuthorization, butBearerTokenAuthenticationPolicyre-added the cached token when the redirecttarget was a different host.
The difference worth flagging: #59606's defect only manifested with a non-default opt-in
(
HttpPipelineTransportOptions.IsClientRedirectEnabled = true). In Go, redirect-following is on bydefault via the standard library and there is no policy of any kind, so the same structural gap is
reachable in the default configuration. .NET's non-bearer policies
(
AzureKeyCredentialPolicy,AzureSasCredentialSynchronousPolicy) have the same shape as Go's, butthey're unreachable there precisely because auto-redirect is off.
How can we reproduce it?
Self-contained — no Azure resources and no network access. Two local servers: a "service" that answers
302with aLocationpointing at an "attacker" host, and the attacker host, which prints everyheader it receives. The request goes through
azcore's default transport.Output (re-run today against azcore v1.22.0 / go1.26.5)
Control:
Authorizationis correctly stripped in every variant, which shows the harness canobserve stripping — so the forwarded headers are a real result rather than a test artifact. In
variant C the credential headers are absent because they were not set, confirming the harness reports
only what actually arrives.
I'm happy to attach the full harness (
go.mod+ a singlemain.go) or open a PR with it as a test,whichever is more useful to you.
Suggested fix
Set an explicit
CheckRedirectondefaultHTTPClient:This matches Rust (
Policy::none()) and .NET (AllowAutoRedirect = false), and the Azure RESTguidelines discourage 3xx from services anyway. If following redirects needs to be preserved, the
Python/Java shape works too: on a hop where
(scheme, host, port)changes, drop the Azure credentialheaders and
Referer.Happy to send a PR for either shape if you'd prefer that to an issue.
Anything we should know about your environment?
Nothing unusual — Windows, Go 1.26.5, module mode, no proxy.
Scope, stated honestly
I have not demonstrated an open redirect or
Locationinjection on a live Azure first-partyservice, and I did not test against any real Azure endpoint. What I'm reporting is that
azcoreprovides no protection if a cross-host redirect occurs, in a codebase where every sibling SDK
deliberately provides one. If the position is that no in-scope service can be made to emit a
cross-host 3xx, that reduces the practical impact considerably and this is just a
defence-in-depth/consistency gap — which still seems worth closing given the one-line fix.
I searched existing issues and PRs before filing: no issue or PR in this repo mentions
CheckRedirect, and #20830 ("Enhance tests for redirect policy", closed) refers to a policy thatdoesn't exist in the Go SDK. Apologies if I missed a prior discussion.