codewithsruthi opened a new pull request, #72398:
URL: https://github.com/apache/airflow/pull/72398
## Summary
- Extra headers and Connection BasicAuth were forwarded by aiohttp on
cross-origin redirects.
- `HttpAsyncHook` now walks redirects and strips Extra names
(case-insensitive) plus auth when the origin changes.
- Sync `HttpHook` is unchanged; tests, docs, and changelog are updated.
This PR is **async-only**. It does not change sync `HttpHook`. Other
in-flight work already covers the same leak from other angles:
- related: #72165 — sync `HttpHook` Extra-header strip (approved)
- related: #70000 — sync `HttpHook` plus an async path (open)
- #70641 is a closed earlier async-only attempt
closes: #70164
## Why it happened
HTTP Connection Extra is documented as a place to put credential headers
(`X-API-Key`, `Private-Token`, and similar) alongside login/password.
`HttpAsyncHook.config` copies every non-reserved Extra key into request headers
and attaches Connection BasicAuth when login is set.
aiohttp follows redirects by default. It strips the literal `Authorization`
header when the host changes, but it does **not** strip Connection Extra
headers the way `requests` strips auth. Extra keys stay on the session and are
replayed to the redirect target. Connection BasicAuth is also applied on walked
hops unless the hook drops it.
The destination does not have to be attacker-controlled. An API that 302s a
download to a CDN or object store still receives the Connection's Extra headers
and BasicAuth. That is the leak this PR closes for `HttpAsyncHook`.
## What changed
Scope is `HttpAsyncHook` only. Sync `HttpHook` is untouched so this can land
beside #72165 and #70000 without rewriting the sync session.
### `providers/http/src/airflow/providers/http/hooks/http.py`
- Helpers: `_redirect_leaves_origin`, `_drop_connection_headers`,
`_get_redirect_location`, plus `_DEFAULT_ASYNC_MAX_REDIRECTS = 10` (aiohttp's
default) and `_REDIRECT_STATUSES` `{301, 302, 303, 307, 308}`.
- Origin rule = `requests.Session.should_strip_auth`: a hostname change
leaves the origin; `http` on port 80/default to `https` on port 443/default
does not; any other scheme or port change does.
- `SessionConfig.connection_headers` records Extra header names after
reserved request options (`stream`, `cert`, `proxies`/`proxy`, `timeout`,
`verify`/`verify_ssl`, `allow_redirects`, `max_redirects`, `trust_env`,
`check_response`) are popped.
- `HttpAsyncHook.config` stores those Extra names on `connection_headers`.
- `AsyncHttpSession._send` is the walker. It forces `allow_redirects=False`
on the slow path so Extra cannot re-enable native aiohttp follow.
### Tests, mock, docs
- `providers/http/tests/unit/http/hooks/test_http.py` — async redirect cases
and `_redirect_leaves_origin` units.
- `devel-common/src/tests_common/test_utils/aiohttp.py` — `headers` and
`release()` on `MockAiohttpClientResponse`.
- `providers/http/docs/connections/http.rst` — Extra headers are sent to the
Connection host and are not forwarded when a redirect changes host.
- `providers/http/docs/changelog.rst` — warning that `HttpAsyncHook` no
longer forwards Extra headers or BasicAuth when a redirect leaves the
Connection host.
### Fast path vs slow path
- **Fast path:** no Extra header names, or `allow_redirects` is false → one
aiohttp call, native redirect behavior unchanged.
- **Slow path:** Extra header names exist and redirects are allowed → walk
hops in the hook.
### What is stripped vs kept
When `_redirect_leaves_origin` is true:
- **Stripped:** Extra header names, compared case-insensitively, and
Connection BasicAuth (`auth` omitted on the next hop).
- **Kept:** caller-supplied headers (for example `X-Request-Id`); Extra
headers and BasicAuth on same-origin hops, including the default-port `http` →
`https` upgrade.
`max_redirects` defaults to **10**. Exceeding it raises
`aiohttp.TooManyRedirects` (surfaced as `HttpErrorException`).
## How it does it
aiohttp has no `requests.Session.rebuild_auth` callback, so the hook walks
the chain only when Extra headers exist and redirects are allowed:
1. Detect Extra header names from `SessionConfig.connection_headers`
(lowercased for comparison).
2. If there are none, or `allow_redirects` is false, take the fast path.
3. Otherwise force `allow_redirects=False` so Extra cannot re-enable native
follow, then walk hops.
4. Resolve the next URL with `urljoin` (relative `Location` stays on the
current origin).
5. If `_redirect_leaves_origin(current, next)`: drop Extra names
(case-insensitive) and omit auth.
6. If hop count reaches `max_redirects` (default 10), release the response
and raise `TooManyRedirects`.
7. Missing `Location`/`location` returns the 3xx as-is.
## Test plan
Already passed (82 tests):
```bash
uv run --project providers/http pytest
providers/http/tests/unit/http/hooks/test_http.py -xvs
```
Cases covered:
- No Extra headers → native aiohttp redirects (`allow_redirects` left unset).
- Extra names recorded; reserved keys such as `max_redirects` are options,
not headers.
- Cross-host / hostname change: Extra + BasicAuth stripped; caller
`X-Request-Id` kept.
- Same-host and relative `Location`: Extra + BasicAuth kept.
- Default-port `http` → `https` and `http:80` → `https:443`: Extra +
BasicAuth kept.
- Port change and non-default-port upgrade: Extra + BasicAuth stripped.
- `allow_redirects=False` does not walk.
- Missing Location returns the 3xx; lowercase `location` is read.
- `max_redirects` 0 and 1 raise when the chain is longer.
- `_redirect_leaves_origin` matches the `should_strip_auth` origin rule.
---
##### Was generative AI tooling used to co-author this PR?
- [X] Yes — Cursor Grok 4.6
Generated-by: Cursor Grok 4.6 following [the
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]