Restrict exception-node deserialization to known classes without importing the stored name - #68511
Restrict exception-node deserialization to known classes without importing the stored name#68511potiuk wants to merge 3 commits into
Conversation
3baa424 to
0e8d29d
Compare
0e8d29d to
653105f
Compare
|
@ashb - would that be a good enough check ? |
653105f to
2523f45
Compare
8719b77 to
83485d9
Compare
|
Moving to 3.3.1 as we are still awaiting reviews and I am about to get rc1 out |
|
Gentle nudge on this one — it's a small, security-flavoured change to exception-node deserialization (stops Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting |
|
I’m not exactly sure but this can potentially block exception classes that the user creates but not eagerly imported? Like lazy imports in a plugin? I did not think it entirely though to be honest, but building an allow list on launch smells wrong in a dynamic environment like Python. |
IMHO it prevents some classes of issues - especially that we ware taking about deserialisation - so potentially passing an exception that has not been imported by Scheduler yet. It is a "defense-in-depth" really, nothing exploitable. |
83485d9 to
6909526
Compare
amoghrajesh
left a comment
There was a problem hiding this comment.
@potiuk I have a qn here.
b57a1fd to
63bde28
Compare
|
Reworked the mechanism off the back of this round of review — the approach changed, so a summary rather than a diff description. The prebuilt allow-list is gone. Resolution now splits the stored name, looks the module up in That drops three separate problems the map had:
The property the change exists for is unchanged: nothing in the stored blob can cause an import. A module that is not already loaded does not resolve, and the class is read from This also answers the concern raised earlier in this thread about lazily-imported plugin exceptions: an allow-list built at launch was the wrong shape for that, and there no longer is one. Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting |
63bde28 to
d364918
Compare
|
All review threads are addressed and resolved. The mechanism changed rather than being patched: the prebuilt allow-list is gone in favour of resolving the stored name against This one has never had an approval; a look from any of you would let it land for 3.3.1. Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting |
amoghrajesh
left a comment
There was a problem hiding this comment.
Some nits, otherwise LGTM
| @@ -664,9 +701,14 @@ def deserialize(cls, encoded_var: Any) -> Any: | |||
| kwargs = deser["kwargs"] | |||
| del deser | |||
| if type_ == DAT.AIRFLOW_EXC_SER: | |||
There was a problem hiding this comment.
I thought there was another pr where we removed this entirely?
There was a problem hiding this comment.
Not in main at least. In a number of places this serialization is left for migration purposes I think. And yes I already raised a question while working on a number of those cases that having a more general solution where we get rid of similar serialization issues would be great. But I do not think we are there yet - cc: @kaxil @amoghrajesh - I think we can discuss it separately
There was a problem hiding this comment.
Yes — #68662. It removes the encode side entirely and makes decode legacy-only, returning str(BaseException(*args)) without resolving or calling anything.
That is a stronger position than this PR, which still resolves a payload-supplied name — constrained to loaded AirflowException subclasses — and then calls it. The cost is that a legacy node deserializes to a string rather than an exception object.
So the real question is whether anything still needs a real exception object back. If not, #68662 is the better fix and this should close in its favour. If something does, this keeps round-trip fidelity and #68662 breaks it.
No attachment to this one either way — flagging it so the two do not both land.
Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting
… name
When deserializing AIRFLOW_EXC_SER / BASE_EXC_SER nodes, BaseSerialization
resolved the exception class with import_string() on a name taken from the
serialized blob. Resolve it against in-memory classes instead, so a stored
DAG never imports a class named in the blob:
- AIRFLOW_EXC_SER: look the name up in a map of loaded AirflowException
subclasses, built once from the in-memory subclass tree; a name that is
not a registered AirflowException subclass is rejected.
- BASE_EXC_SER: resolve against the fixed {KeyError, AttributeError} set
that the encoder is the only producer of.
Unknown or disallowed names raise DeserializationError instead of being
imported. The trigger-node branch is handled separately.
Generated-by: Claude Opus 4.8 following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions
…rebuilt map A prebuilt map keyed on each class's __module__ cannot follow the re-exports that import_string used to follow. These exceptions moved to airflow.sdk.exceptions in 3.2.0, so every blob written by 3.0/3.1 names airflow.exceptions.<Name> and would stop deserializing on upgrade. Reading the name out of an already-loaded module keeps that working, needs no cache to go stale when a provider or plugin registers its own subclass late, and still refuses to import anything the stored blob names.
d364918 to
894ea7c
Compare
Review feedback: the explanation was longer than the behaviour it describes.
When deserializing
AIRFLOW_EXC_SER/BASE_EXC_SERnodes, the exception class was resolved byimport_string()on a name taken from the serialized blob. This resolves it against in-memory classes instead, so deserializing a stored DAG never imports a class named in the blob:AIRFLOW_EXC_SER— looked up in a map of loadedAirflowExceptionsubclasses, built once from the in-memory subclass tree and never rebuilt; a name that isn't a registeredAirflowExceptionsubclass is rejected.BASE_EXC_SER— resolved against the fixed{KeyError, AttributeError}set, which is all the encoder ever emits for this node type.Unknown or disallowed names raise
DeserializationErrorinstead of being imported. The trigger-node branch is handled separately in #67926.Tests
AIRFLOW_EXC_SERname that isn't a registeredAirflowExceptionsubclass is rejected (not imported)AirflowExceptionsubclass round-trips via the registryBASE_EXC_SERname outside{KeyError, AttributeError}(e.g.eval,ValueError) rejectedKeyError/AttributeErrorstill round-tripWas generative AI tooling used to co-author this PR?
Generated-by: Claude Opus 4.8 following the guidelines at
https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions