Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date - #67628

Merged
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date
Jun 21, 2026
Merged

Enforce ti:self scope on /execution/task-reschedules/{ti}/start_date#67628
potiuk merged 1 commit into
apache:mainfrom
potiuk:enforce-ti-self-scope-on-task-reschedules-start-date

Conversation

@potiuk

@potiukpotiuk commented May 27, 2026

Copy link
Copy Markdown
Member

Three sibling per-task-instance routers under airflow.api_fastapi.execution_api.routes opt into the ti:self JWT scope, which "verifies that the token's sub claim matches the {task_instance_id} path parameter, preventing a worker from accessing another task's endpoints" (per security/jwt_token_authentication.html): task_instances.py, hitl.py, and task_state.py. The task_reschedules.py router for GET /execution/task-reschedules/{task_instance_id}/start_date was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.

This change adds the standard dependencies=[Security(require_auth, scopes=["ti:self"])] to the router declaration — the same pattern the three sibling routers already use. One new regression test under TestGetRescheduleStartDate exercises the mismatched-subject path and asserts 403.

Acknowledgement

Thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing scope enforcement on this endpoint, which prompted this hardening change.

Test plan

  • New regression test test_mismatched_subject_is_rejected asserts a mismatched JWT subject is rejected with 403 on the concrete route.
  • Existing TestGetRescheduleStartDate tests still pass (the conftest client fixture auto-matches the JWT subject to the path parameter, so happy-path tests are unaffected).
  • prek run --from-ref main --to-ref HEAD --stage pre-commit clean on touched files.
  • prek run --from-ref main --to-ref HEAD --stage manual clean on touched files.
Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 4.7 (1M context)

Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

@ashbashb 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.

As i said before: We should also add a dual to existing test_ti_self_routes_have_task_instance_id_param that tests that all routes with {task_instance_id} param in them either have ti:self or an explicit exclusion.

That is a much more useful test than re-testing the effect of the scope on one end point

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from ebe21d7 to 855f993CompareJune 5, 2026 02:05
@potiuk

Copy link
Copy Markdown
MemberAuthor

Done — replaced the per-endpoint test with the structural dual you described. test_routes_with_task_instance_id_param_enforce_ti_self now asserts every operation exposing a {task_instance_id} path param requires the ti:self scope (or is on an explicit, currently-empty TI_ID_ROUTES_WITHOUT_TI_SELF allowlist), and dropped test_mismatched_subject_is_rejected.

One implementation note: I couldn't do it by walking app.routes like the existing forward test — the execution API assembles routes per version, so in the test harness static route introspection only surfaces a single top-level route. So it checks the served OpenAPI spec (security per operation) across every version in the bundle instead. Verified it's non-vacuous (12 such paths) and that it actually catches the gap: reverting the ti:self dependency makes it fail, listing GET /task-reschedules/{task_instance_id}/start_date across all versions.

Side note from that: the existing test_ti_self_routes_have_task_instance_id_param (forward direction) appears to be effectively vacuous for the same route-introspection reason — happy to convert it to the OpenAPI approach in a follow-up if you agree.


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

@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch 2 times, most recently from d24359b to 70b90cfCompareJune 5, 2026 16:49
Three sibling per-task-instance routers under `airflow.api_fastapi.execution_api.routes` opt into the `ti:self` JWT scope, which verifies that the token's `sub` claim matches the `{task_instance_id}` path parameter, preventing a worker from accessing another task's endpoints: `task_instances.py`, `hitl.py`, and `task_state.py`. The `task_reschedules.py` router for `GET /execution/task-reschedules/{task_instance_id}/start_date` was missing that scope, so any authenticated worker could read the first reschedule timestamp of any task instance in the deployment by passing that task instance's UUID in the URL path.
This change adds the standard `dependencies=[Security(require_auth, scopes=["ti:self"])]` to the router declaration — the same pattern the three sibling routers already use. One new regression test under `TestGetRescheduleStartDate` exercises the mismatched-subject path and asserts 403.
Reference: airflow-s/airflow-s#406
Generated-by: Claude Opus 4.7 (1M context) following the guidelines at https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
@potiuk
potiukforce-pushed the enforce-ti-self-scope-on-task-reschedules-start-date branch from 70b90cf to eef3797CompareJune 5, 2026 16:53
@potiuk
potiuk requested a review from ashbJune 11, 2026 17:33
@potiuk

Copy link
Copy Markdown
MemberAuthor

@ashb — circling back: the structural dual you asked for went in on the 5th (test_routes_with_task_instance_id_param_enforce_ti_self) — it walks every {task_instance_id} operation across all API versions and asserts each carries ti:self or sits in an explicit TI_ID_ROUTES_WITHOUT_TI_SELF exclusion set. PR's mergeable + green. Could you take another look / clear the change-request? Thanks!

@potiukpotiuk added this to the Airflow 3.3.0 milestone Jun 21, 2026
@potiuk
potiuk merged commit ab68720 into apache:mainJun 21, 2026
90 checks passed
@potiuk
potiuk deleted the enforce-ti-self-scope-on-task-reschedules-start-date branch June 21, 2026 22:34
@potiuk

Copy link
Copy Markdown
MemberAuthor

Crediting the reporter: thanks to Harish Kolla (independent security researcher, GitHub @Har1sh-k) for reporting the missing ti:self scope enforcement on GET /execution/task-reschedules/{task_instance_id}/start_date that prompted this hardening change. 🙏

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:APIAirflow's REST/HTTP APIarea:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@potiuk@ashb@bugraoz93