Skip to content

Stop backfill endpoints disclosing ids across Dags you cannot see - #71113

Merged
pierrejeambrun merged 5 commits into
apache:mainfrom
rjgoyln:fix-backfill-authz-existence-oracle
Aug 27, 2026
Merged

pierrejeambrun merged 5 commits into
apache:mainfrom
rjgoyln:fix-backfill-authz-existence-oracle

Conversation

@rjgoyln

@rjgoyln rjgoyln commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

The five backfill routes that name a backfill in their path fell back to the dag_id supplied on the request — body or query string — whenever the id matched no row, so an unknown id and a backfill on a Dag the caller cannot see answered differently, 404 against 403. That difference enumerates backfill ids across Dags.

The path names the backfill, so its row is now the only thing those routes authorize against, and an id the caller may not see answers exactly as a missing one does. A caller who may read the Dag keeps the Forbidden answer on a write they are not allowed: they can already list that Dag's backfills, so hiding it would only cost them the reason their request was refused. The list, create and dry-run routes name no backfill in their path and keep authorizing off the body.

Behavior change

On those five routes a Dag the caller cannot read now answers 404 rather than 403.

closes: #71080


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

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

The routes that name a backfill in their path resolved the authorization
subject from the request when the id matched no row, so a caller could tell
"no such backfill" apart from "a backfill you may not see" and enumerate ids
belonging to Dags they have no access to.

The path names the backfill, so nothing the caller supplies alongside it can
be the subject, and an id they may not see has to answer exactly as a missing
one does. A caller who may read the Dag keeps the Forbidden answer: they can
already list that Dag's backfills, so hiding it would only cost them the
reason their request was refused.

closes: apache#71080
@boring-cyborg boring-cyborg Bot added the area:API Airflow's REST/HTTP API label Aug 4, 2026
@potiuk potiuk added the ready for maintainer review Set by triage when all criteria pass, or by the fork CI reconciler when fork CI is green. label Aug 13, 2026

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a clean fix and it meets all three acceptance criteria from #71080 — the path id is the only authorization subject on those five routes, unknown and unreadable are indistinguishable, and list/create/dry-run keep authorizing off the request. Nice touches: all five routes already declared 404 in create_openapi_http_exception_doc, so nothing needs regenerating, and the lookup narrowed from a full Backfill row to select(Backfill.dag_id).

Approving. One thing worth a follow-up commit here, and two nits you can take or leave.

The 404 detail string changed on four routes too — worth a newsfragment line

See the inline comment on security.py. The newsfragment covers 403 → 404 for unreadable Dags, but for an authorized caller the message for an unknown id also changed, because the dependency now answers before the handler runs:

Route Before After
GET /backfills/{id} Backfill not found unchanged
GET /backfills/{id}/dag_runs Backfill with id 999 not found Backfill not found
PUT .../pause, .../unpause, .../cancel Could not find backfill with id 999 Backfill not found

Nothing caught it because test_list_backfill_dag_runs_not_found asserts only the status code and the three PUT routes have no unknown-id detail assertion.

Smaller observations

  • See the inline comment on security.py about the second is_authorized_dag call.
  • See the inline comment on the new dag_reader_test_client fixture.

This review was drafted by an AI-assisted tool and
confirmed by an Airflow maintainer. The maintainer
approving this PR has read the findings and signed off. If
something feels off, please reply on the PR and a maintainer
will follow up.

More on how Airflow handles maintainer review:
contributing-docs/05_pull_requests.rst.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

For an id named in the path the access dependency now answers before the handler
does, so the three different messages the handlers used to describe that single
condition are no longer what a caller sees. A client matching on the detail string
would otherwise have to know which route it hit, and which of the two places
answered.
@rjgoyln

rjgoyln commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

This is a clean fix and it meets all three acceptance criteria from #71080 — the path id is the only authorization subject on those five routes, unknown and unreadable are indistinguishable, and list/create/dry-run keep authorizing off the request. Nice touches: all five routes already declared 404 in create_openapi_http_exception_doc, so nothing needs regenerating, and the lookup narrowed from a full Backfill row to select(Backfill.dag_id).

Approving. One thing worth a follow-up commit here, and two nits you can take or leave.

The 404 detail string changed on four routes too — worth a newsfragment line

See the inline comment on security.py. The newsfragment covers 403 → 404 for unreadable Dags, but for an authorized caller the message for an unknown id also changed, because the dependency now answers before the handler runs:

Route Before After
GET /backfills/{id} Backfill not found unchanged
GET /backfills/{id}/dag_runs Backfill with id 999 not found Backfill not found
PUT .../pause, .../unpause, .../cancel Could not find backfill with id 999 Backfill not found
Nothing caught it because test_list_backfill_dag_runs_not_found asserts only the status code and the three PUT routes have no unknown-id detail assertion.

Smaller observations

  • See the inline comment on security.py about the second is_authorized_dag call.
  • See the inline comment on the new dag_reader_test_client fixture.

This review was drafted by an AI-assisted tool and
confirmed by an Airflow maintainer. The maintainer
approving this PR has read the findings and signed off. If
something feels off, please reply on the PR and a maintainer
will follow up.

More on how Airflow handles maintainer review:
contributing-docs/05_pull_requests.rst.

Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Thanks — the detail string was the part I missed, and it is a real break for anyone matching on it.

Follow-up pushed: all five routes now raise security.BACKFILL_NOT_FOUND (the constant lost its underscore so the routes can share it), so the one condition has one message whether the dependency or the handler answers, and the newsfragment records the change with the old strings. I also added the missing unknown-id detail assertions on /dag_runs, unpause and cancel.

On the second is_authorized_dag call: I left it as two calls because the decision is three-way rather than a single gate — allow, 403 when the caller may read the Dag but not write it, 404 otherwise. batch_is_authorized_dag collapses the set to one boolean, so a False still would not say which of the two failed, and asking for method and GET together would turn a can-edit-without-can-read role from allowed into refused, which the allow-first ordering deliberately permits. Happy to revisit if you would rather the pair were required.

On the fixture: agreed, and I have kept it local here. A role_test_client factory in conftest.py folding all three is a tidy separate change — I can open it if that is useful.

@pierrejeambrun pierrejeambrun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@pierrejeambrun pierrejeambrun added this to the Airflow 3.3.2 milestone Aug 27, 2026
@pierrejeambrun
pierrejeambrun merged commit 02dbd2f into apache:main Aug 27, 2026
79 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Backport failed to create: v3-3-test. View the failure log Run details

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
❌ v3-3-test Commit Link

You can attempt to backport this manually by running:

cherry_picker 02dbd2f v3-3-test

This should apply the commit to the v3-3-test branch and leave the commit in conflict state marking
the files that need manual conflict resolution.

After you have resolved the conflicts, you can continue the backport process by running:

cherry_picker --continue

If you don't have cherry-picker installed, see the installation guide.

pierrejeambrun added a commit that referenced this pull request Aug 28, 2026
…1113) (#72192)

* Stop backfill endpoints disclosing which backfill ids exist across Dags

The routes that name a backfill in their path resolved the authorization
subject from the request when the id matched no row, so a caller could tell
"no such backfill" apart from "a backfill you may not see" and enumerate ids
belonging to Dags they have no access to.

The path names the backfill, so nothing the caller supplies alongside it can
be the subject, and an id they may not see has to answer exactly as a missing
one does. A caller who may read the Dag keeps the Forbidden answer: they can
already list that Dag's backfills, so hiding it would only cost them the
reason their request was refused.

closes: #71080

* Add newsfragment for the backfill authorization change

* Give every backfill route one answer for an id that is not there

For an id named in the path the access dependency now answers before the handler
does, so the three different messages the handlers used to describe that single
condition are no longer what a caller sees. A client matching on the detail string
would otherwise have to know which route it hit, and which of the two places
answered.

(cherry picked from commit 02dbd2f)

Co-authored-by: rjgoyln <151457491+rjgoyln@users.noreply.github.com>
vatsrahul1001 pushed a commit that referenced this pull request Sep 7, 2026
…1113) (#72192)

* Stop backfill endpoints disclosing which backfill ids exist across Dags

The routes that name a backfill in their path resolved the authorization
subject from the request when the id matched no row, so a caller could tell
"no such backfill" apart from "a backfill you may not see" and enumerate ids
belonging to Dags they have no access to.

The path names the backfill, so nothing the caller supplies alongside it can
be the subject, and an id they may not see has to answer exactly as a missing
one does. A caller who may read the Dag keeps the Forbidden answer: they can
already list that Dag's backfills, so hiding it would only cost them the
reason their request was refused.

closes: #71080

* Add newsfragment for the backfill authorization change

* Give every backfill route one answer for an id that is not there

For an id named in the path the access dependency now answers before the handler
does, so the three different messages the handlers used to describe that single
condition are no longer what a caller sees. A client matching on the detail string
would otherwise have to know which route it hit, and which of the two places
answered.

(cherry picked from commit 02dbd2f)

Co-authored-by: rjgoyln <151457491+rjgoyln@users.noreply.github.com>
imrichardwu pushed a commit to imrichardwu/airflow that referenced this pull request Sep 11, 2026
…ache#71113)

* Stop backfill endpoints disclosing which backfill ids exist across Dags

The routes that name a backfill in their path resolved the authorization
subject from the request when the id matched no row, so a caller could tell
"no such backfill" apart from "a backfill you may not see" and enumerate ids
belonging to Dags they have no access to.

The path names the backfill, so nothing the caller supplies alongside it can
be the subject, and an id they may not see has to answer exactly as a missing
one does. A caller who may read the Dag keeps the Forbidden answer: they can
already list that Dag's backfills, so hiding it would only cost them the
reason their request was refused.

closes: apache#71080

* Add newsfragment for the backfill authorization change

* Give every backfill route one answer for an id that is not there

For an id named in the path the access dependency now answers before the handler
does, so the three different messages the handlers used to describe that single
condition are no longer what a caller sees. A client matching on the detail string
would otherwise have to know which route it hit, and which of the two places
answered.
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 ready for maintainer review Set by triage when all criteria pass, or by the fork CI reconciler when fork CI is green.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backfill authorization falls back to the request body when the backfill does not exist

3 participants