Skip to content

[fix][sec] Upgrade to async-http-client 2.14.5 to address CVE-2026-40490#25546

Merged
nodece merged 4 commits intoapache:masterfrom
lhotari:lh-upgrade-async-http-client
Apr 17, 2026
Merged

[fix][sec] Upgrade to async-http-client 2.14.5 to address CVE-2026-40490#25546
nodece merged 4 commits intoapache:masterfrom
lhotari:lh-upgrade-async-http-client

Conversation

@lhotari
Copy link
Copy Markdown
Member

@lhotari lhotari commented Apr 16, 2026

Motivation

async-http-client 2.12.4 contains GHSA-cmxv-58fp-fm3g / CVE-2026-40490.

The CVE fix in async-http-client 2.14.5 strips the Authorization and Proxy-Authorization headers on cross-origin redirects (different scheme/host/port) when AHC's built-in follow-redirect is enabled. There is no opt-out.

Impact on Pulsar:

  • pulsar-client-admin: safe. AsyncHttpConnector already sets setFollowRedirect(false) and runs a manual redirect loop, so AHC's new stripping never runs. No test previously guarded this invariant, though — a future regression to built-in follow-redirect would silently break cross-broker admin calls.
  • pulsar-client HttpLookupServiceHttpClient: broken. HttpClient set setFollowRedirect(true) and only invoked authentication.newRequestHeader(...) once on the initial request. After the AHC upgrade, HttpLookupService.getBroker(...) and getPartitionedTopicMetadata(...) would receive 401 on any cross-broker 307 redirect (Pulsar's TopicLookupBase routinely redirects to a different broker's httpUrl/httpUrlTls). Affects AuthenticationToken, AuthenticationBasic, AuthenticationOAuth2, AuthenticationAthenz, AuthenticationSasl, and any custom plugin that sets the Authorization header.

Modifications

  • Upgrade async-http-client from 2.12.4 to 2.14.5 (note: skips 2.12.5 — explanation).
  • pulsar-client HttpClient: disable AHC's built-in follow-redirect (setFollowRedirect(false)) and drive the redirect loop in Pulsar code. The new executeGet(...) / handleRedirect(...) re-invokes authentication.getAuthData(...) + authentication.newRequestHeader(...) per hop, so the Authorization header (and per-hop auth data — useful for OAuth2 / SASL) is applied to the redirect target. Handles 301/302/303/307/308, bounded by conf.getMaxLookupRedirects(). Mirrors the pattern already used by pulsar-client-admin's AsyncHttpConnector.
  • Minor: replaced the opaque "PulsarClientImpl" setup-failure log in HttpClient.get(...) with a descriptive message.

Verifying this change

This change added tests and can be verified as follows:

  • HttpClientTest.testCrossOriginRedirectCarriesAuthorizationHeader (new, pulsar-client) — two WireMock servers on different ports; serverA returns 307 to serverB; serverB requires Authorization: Bearer test-token. Fails without the fix (401 from serverB — AHC 2.14.5 stripped the header), passes with the fix.
  • HttpLookupServiceTest.testGetBrokerFollowsCrossOriginRedirect (new, pulsar-client) — end-to-end HttpLookupService.getBroker(TopicName) against the same cross-origin setup.
  • AsyncHttpConnectorTest.testAuthorizationHeaderOnCrossOriginRedirect (new, pulsar-client-admin) — locks in that the admin connector continues to forward Authorization across cross-origin redirects. Passes without any admin code change — guards a future regression (e.g., flipping setFollowRedirect back to true).

Added wiremock as a test dependency in pulsar-client (already available in the version catalog and in use by pulsar-client-admin).

Does this pull request potentially affect one of the following parts:

  • Dependencies (add or upgrade a dependency) — async-http-client 2.12.4 → 2.14.5; wiremock added to pulsar-client test scope.
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

@massakam
Copy link
Copy Markdown
Contributor

I have a question.

This vulnerability allowed async-http-client to send the Authorization header to the redirected URL without removing it when followRedirect(true). The fixed async-http-client will now automatically remove the Authorization header if the scheme, domain, or port of the redirected URL differs from the original. Would this behavior cause problems in Pulsar?

In Pulsar, HTTP redirects from one broker to another are common, and the domain of the URL changes during this process. In addition, the following authentication plugins use the Authorization header:

@lhotari lhotari changed the title [fix][sec] Upgrade to async-http-client 2.14.5 to address CVE [fix][sec] Upgrade to async-http-client 2.14.5 to address CVE-2026-40490 Apr 17, 2026
@lhotari
Copy link
Copy Markdown
Member Author

lhotari commented Apr 17, 2026

I have a question.

This vulnerability allowed async-http-client to send the Authorization header to the redirected URL without removing it when followRedirect(true). The fixed async-http-client will now automatically remove the Authorization header if the scheme, domain, or port of the redirected URL differs from the original. Would this behavior cause problems in Pulsar?

@massakam good question. I'll address this. Claude made this analysis based on your question:

Impact of async-http-client 2.14.5 Authorization-stripping fix on Pulsar

Short answer

Yes, this would break pulsar-client's HttpLookupService, but pulsar-client-admin is safe. The two HTTP paths handle redirects very differently.

The two cases

1. pulsar-client-admin → Safe

AsyncHttpConnector explicitly calls setFollowRedirect(false) on the AHC config and implements its own redirect loop. On each hop it rebuilds the request through prepareRequest(...), which invokes the Authentication plugin's authenticate(...) / newRequestHeader(...) again and re-injects the Authorization header. Because AHC never sees followRedirect=true, Redirect30xInterceptor never runs, and its new stripping logic never fires. Cross-broker 307s keep working.

The proxy path (AdminProxyHandler with Jetty HttpClient) has its own copyRequest override that explicitly carries Authorization across 307s — also unaffected.

2. pulsar-client HttpLookupService → Breaks

pulsar-client/.../impl/HttpClient.java configures AHC with setFollowRedirect(true) and setMaxRedirects(...), i.e. AHC does the redirect itself. The Authorization header is added once on the initial request from authentication.newRequestHeader(...); there's no rebuild-per-hop, so it relies entirely on AHC carrying the header across the redirect.

This is exactly the code path the fix changes. Redirect30xInterceptor.propagatedHeaders(...) now strips Authorization (and Proxy-Authorization) when:

  • !request.getUri().isSameBase(newUri) — scheme, host, or port differs, or
  • HTTPS → HTTP downgrade, or
  • stripAuthorizationOnRedirect=true (default is false, so same-origin redirects are not affected).

Pulsar's HTTP lookup redirects (TEMPORARY_REDIRECT in TopicLookupBase, cross-cluster lookups, bundle-ownership redirects) routinely point to a different broker's httpUrl / httpUrlTls — different host, sometimes different port, sometimes different scheme. Same-path redirects don't exist here by design; the point is to send the client to the broker that owns the bundle.

Consequence: after upgrade, the redirected lookup request arrives at broker2 with no Authorization header → 401 → lookup fails. This hits every auth plugin that uses Authorization: AuthenticationToken, AuthenticationBasic, AuthenticationOAuth2, AuthenticationAthenz, AuthenticationSasl (SASL state header), and anything custom that sets it via newRequestHeader(...).

Note: stripAuthorizationOnRedirect does not help here — it's a tightening switch, not a disabling one. Setting it false (the default) still strips on cross-origin redirects; there is no opt-out for that.

Fix options for Pulsar

The cleanest fix is to make HttpLookupService's client match the admin-client pattern: turn off AHC's follow-redirect and do the redirect loop in Pulsar code, re-invoking authentication.newRequestHeader(...) per hop. That also gives correct behavior for auth plugins that need per-request tokens (OAuth2 refresh, SASL challenge-response) — which the current "set once, let AHC replay" approach already handles sub-optimally.

A narrower workaround would be a small interceptor/filter that re-applies the auth header after AHC's redirect, but given AHC strips it in propagatedHeaders before issuing the redirected request, you'd need to hook in at the right point — doing it in Pulsar's own redirect loop is simpler.

Worth filing as a blocker on any PR that bumps AHC ≥ 2.14.5 in pulsar-client.

@massakam
Copy link
Copy Markdown
Contributor

@lhotari Thank you for your reply!

  1. pulsar-client-admin → Safe

I understand that this upgrade will not cause regressions. On the other hand, this behavior is not much different from async-http-client before the vulnerability was fixed, so it may be an issue that Pulsar should consider addressing in the future.

  1. pulsar-client HttpLookupService → Breaks

This looks like it will block this upgrade. Are you planning to modify pulser-client in this pull request?

…rization

async-http-client 2.14.5 (CVE-2026-40490 fix) strips the Authorization header on
cross-origin redirects when its built-in follow-redirect is enabled. Pulsar's
HTTP lookup routinely 307-redirects to another broker's httpUrl/httpUrlTls,
which is a different host:port from the client's service URL. After the AHC
upgrade, token/basic/OAuth2-authenticated HTTP lookups would fail with 401.

Disable AHC's follow-redirect in HttpClient and drive the redirect loop in
pulsar-client, re-invoking authentication.newRequestHeader(...) per hop so
Authorization is applied to the redirect target. Mirrors the manual-redirect
pattern already used by pulsar-client-admin's AsyncHttpConnector.

Adds WireMock-based cross-origin redirect tests for HttpClient,
HttpLookupService, and AsyncHttpConnector (the last locks in the admin client's
existing safe behavior).
@lhotari lhotari marked this pull request as draft April 17, 2026 08:46
@lhotari
Copy link
Copy Markdown
Member Author

lhotari commented Apr 17, 2026

This looks like it will block this upgrade. Are you planning to modify pulser-client in this pull request?

yes

@lhotari lhotari marked this pull request as ready for review April 17, 2026 09:13
@lhotari lhotari requested a review from massakam April 17, 2026 09:13
@lhotari
Copy link
Copy Markdown
Member Author

lhotari commented Apr 17, 2026

@massakam PTAL

…lient

The top-level catch in HttpClient.get logged the opaque marker "PulsarClientImpl".
Replace it with a descriptive message so log aggregations actually tell you what
went wrong.
@nodece nodece merged commit a1613bc into apache:master Apr 17, 2026
43 checks passed
lhotari added a commit that referenced this pull request Apr 17, 2026
lhotari added a commit that referenced this pull request Apr 17, 2026
lhotari added a commit that referenced this pull request Apr 17, 2026
lhotari added a commit that referenced this pull request Apr 17, 2026
priyanshu-ctds pushed a commit to datastax/pulsar that referenced this pull request Apr 22, 2026
srinath-ctds pushed a commit to datastax/pulsar that referenced this pull request Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants