Use a structured SecretCache key instead of a concatenated string - #72201
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request fixes a key-collision bug in the Task SDK’s SecretCache by replacing the previous string-concatenated cache key (which was not injective when team_name was optional) with a structured key. This prevents teamless reads/writes/invalidations from accidentally (or maliciously) colliding with team-scoped entries.
Changes:
- Introduce a structured
_CacheKey(NamedTuple) and use it for all cache get/set/pop operations. - Remove the previous
_TEAM_PATTERN+ string composition scheme that could produce collisions. - Add regression tests covering the previously-colliding read/write/invalidate paths and team-name prefix cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
task-sdk/src/airflow/sdk/execution_time/cache.py |
Switch SecretCache internal keying from concatenated strings to a structured _CacheKey to eliminate collisions. |
task-sdk/tests/task_sdk/execution_time/test_cache.py |
Add regression tests ensuring colliding keys no longer allow cross-team access, overwrite, or eviction. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
vincbeck
approved these changes
Aug 28, 2026
vincbeck
left a comment
Contributor
There was a problem hiding this comment.
Indeed, simpler and more robust
The cache key was built by concatenating the prefix, an optional '_{team}_'
segment and the entry key. That mapping is not injective while the team
segment is optional: for a team 'analytics' and key 'DB_PASSWORD' the composed
string is identical to the one produced with no team and key
'_analytics_DB_PASSWORD', so the two entries share a slot. Reads, writes and
invalidations all resolve through the same composed string, so distinct
entries could read, overwrite or evict one another.
The parts are now carried in a _CacheKey NamedTuple, which is injective by
construction and needs no escaping, and names each part at the call site. It
is declared at module level so it pickles by qualified name across the
multiprocessing manager the cache is stored in. The prefixes are private to
this module, so no caller depends on the previous key shape.
Added coverage for entries that shared a slot under the old scheme, on the
read, write and invalidate paths, plus team names that share a prefix.
potiuk
force-pushed
the
fix-secretcache-injective-key
branch
from
August 28, 2026 14:34
78853a2 to
36b440e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SecretCachebuilt its key by concatenating three parts:That mapping is not injective while the team segment is optional. Team
analyticswith keyDB_PASSWORDcomposes to__v__analytics_DB_PASSWORD, and so does no team with key_analytics_DB_PASSWORD— the two entries share a cache slot.Reads, writes and invalidations all resolve through the same composed string, so entries that collide could read, overwrite or evict one another.
The three parts are now kept as a tuple, which is injective by construction and needs no escaping or length-prefixing:
_VARIABLE_PREFIX,_CONNECTION_PREFIXand_TEAM_PATTERNare private to this module and had no callers outside it, so nothing depended on the previous key shape._TEAM_PATTERNis now gone.Tests. Added coverage for entries that shared a slot under the old scheme, on each of the three paths — read, write (the colliding write must not clobber the other entry) and invalidate — plus team names that share a prefix. Reverting the source change fails four of them; they pass with it.
Local: 17 passed in the touched file, 1070 across
task-sdk/tests/task_sdk/execution_time/(11 pre-existingTriggerDagRunOperatorfailures reproduce identically on a clean tree), 18 inairflow-core/tests/unit/always/test_secrets.py. ruff and mypy clean.🤖 Generated with Claude Code