Skip to content

Require trust sentinel for state.user injection in get_user()#66562

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:fix/get-user-trust-marker-for-state-user
May 7, 2026
Merged

Require trust sentinel for state.user injection in get_user()#66562
potiuk merged 1 commit into
apache:mainfrom
potiuk:fix/get-user-trust-marker-for-state-user

Conversation

@potiuk
Copy link
Copy Markdown
Member

@potiuk potiuk commented May 7, 2026

Summary

The get_user() auth dependency in core_api/security.py accepted any value at request.state.user without verification, returning it before JWT signature/expiry/revocation checks. The only legitimate writer (JWTRefreshMiddleware) is one of many possible middlewares — any plugin or unrelated middleware that wrote request.state.user, accidentally or otherwise, would silently bypass JWT validation.

Fix

Defense-in-depth: introduce a private module-level sentinel USER_INJECTED_BY_TRUSTED_MIDDLEWARE and require it to be set at request.state.user_authenticated_via for get_user() to honour the cached user. JWTRefreshMiddleware now stamps the marker alongside the user. Without the marker, get_user() falls through to fresh JWT validation, so a stray state.user = ... write no longer skips auth.

Threat model

This does not defend against a malicious in-process plugin (which can import the sentinel and set it itself) — plugins are trusted code in Airflow's security model. The goal is preventing accidental writes from unrelated middleware silently bypassing auth, which the audit flagged as an undocumented authentication pathway.

Tests cover both the marked-honoured path and the unmarked-fall-through path; the JWTRefreshMiddleware test asserts the marker is stamped.

Reported by

L3 ASVS sweep — apache/tooling-agents#23 (FINDING-133).


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines

The `get_user()` auth dependency in `core_api/security.py` accepted any
value at `request.state.user` without verification, returning it before
JWT signature/expiry/revocation checks. The only legitimate writer
(`JWTRefreshMiddleware`) is one of many possible middlewares — any plugin
or unrelated middleware that wrote `request.state.user`, accidentally or
otherwise, would silently bypass JWT validation.

Defense-in-depth: introduce a private module-level sentinel
`USER_INJECTED_BY_TRUSTED_MIDDLEWARE` and require it to be set at
`request.state.user_authenticated_via` for `get_user()` to honour the
cached user. JWTRefreshMiddleware now stamps the marker alongside the
user. Without the marker `get_user()` falls through to fresh JWT
validation, so a stray `state.user = ...` write no longer skips auth.

This does not defend against a *malicious* in-process plugin (which can
import the sentinel and set it itself); plugins are trusted code in
Airflow's security model. The goal is preventing accidental writes from
unrelated middleware silently bypassing auth, which the audit flagged as
an undocumented authentication pathway.

Tests cover both the marked-honoured path and the unmarked-fall-through
path; the JWTRefreshMiddleware test asserts the marker is stamped.

Reported by the L3 ASVS sweep at apache/tooling-agents#23 (FINDING-133).
@vincbeck
Copy link
Copy Markdown
Contributor

vincbeck commented May 7, 2026

Does it make it more secure? Any other middleware could also set manually this request.state.user_authenticated_via = USER_INJECTED_BY_TRUSTED_MIDDLEWARE to make it as though it was set by refresh token middleware

@potiuk
Copy link
Copy Markdown
Member Author

potiuk commented May 7, 2026

Does it make it more secure? Any other middleware could also set manually this request.state.user_authenticated_via = USER_INJECTED_BY_TRUSTED_MIDDLEWARE to make it as though it was set by refresh token middleware

It's really so called "defense-in-depth" - of somebody adds a middleware that will inject a user, it won't work

So this is not fixing a security issue per se, but makes the architecture more resilient in case deployment manager makes a mistake.

@vincbeck
Copy link
Copy Markdown
Contributor

vincbeck commented May 7, 2026

I see 👌

@potiuk potiuk merged commit f141e15 into apache:main May 7, 2026
275 of 276 checks passed
@potiuk potiuk deleted the fix/get-user-trust-marker-for-state-user branch May 7, 2026 23:18
jason810496 pushed a commit to jason810496/airflow that referenced this pull request May 11, 2026
…#66562)

The `get_user()` auth dependency in `core_api/security.py` accepted any
value at `request.state.user` without verification, returning it before
JWT signature/expiry/revocation checks. The only legitimate writer
(`JWTRefreshMiddleware`) is one of many possible middlewares — any plugin
or unrelated middleware that wrote `request.state.user`, accidentally or
otherwise, would silently bypass JWT validation.

Defense-in-depth: introduce a private module-level sentinel
`USER_INJECTED_BY_TRUSTED_MIDDLEWARE` and require it to be set at
`request.state.user_authenticated_via` for `get_user()` to honour the
cached user. JWTRefreshMiddleware now stamps the marker alongside the
user. Without the marker `get_user()` falls through to fresh JWT
validation, so a stray `state.user = ...` write no longer skips auth.

This does not defend against a *malicious* in-process plugin (which can
import the sentinel and set it itself); plugins are trusted code in
Airflow's security model. The goal is preventing accidental writes from
unrelated middleware silently bypassing auth, which the audit flagged as
an undocumented authentication pathway.

Tests cover both the marked-honoured path and the unmarked-fall-through
path; the JWTRefreshMiddleware test asserts the marker is stamped.

Reported by the L3 ASVS sweep at apache/tooling-agents#23 (FINDING-133).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants