The vulnerability is a security guard that exists but fails because it wasn't applied uniformly across all code paths when a convenience feature was added. In Apache Airflow, the function _check_forbidden_xcom_keys was already written and working — it prevents attackers from instantiating arbitrary Airflow classes through XCom deserialization. But it was only applied to one deserialization path: when the payload arrives as an object. A second path handling JSON string literals bypassed the check entirely. This is not developers forgetting security; it's security architecture failing to keep pace with feature evolution.
When someone added the string-literal handling path — likely for a legitimate edge case or performance optimization — they weren't thinking about security-sensitive functions they needed to call. The cognitive load of feature implementation lives in a different mental context than security review. Without tooling that flags missing guard calls, these gaps are nearly inevitable.
The fix in 3.3.1 (rejecting reserved XCom keys in string literals) addresses the symptom but not the structural problem: there are multiple deserialization entry points, and security depends entirely on whether a developer remembered to call the right function. This pattern — guards that exist but have gaps — is responsible for more deserialization vulnerabilities than completely unguarded code.
The CVSS 5.4 score badly understates the real blast radius. In Airflow, arbitrary airflow.* class instantiation means accessing DAG execution, Variables, connections, and plugins. With XCom as the bloodstream between tasks — flowing across DAG boundaries and task groups — an attacker chaining this through legitimate-looking tasks can reach secrets, trigger operations, and in multi-tenant deployments, reach cross-tenant data.
The guard likely delayed discovery of this bypass. Once reviewers saw _check_forbidden_xcom_keys existed, they assumed coverage and looked elsewhere. The guard didn't just fail passively; it actively created a blind spot.
Architectural fixes like unified deserialization entry points or type-system enforcement sound right but will erode over time — history shows developers subclass around constraints when legitimate use cases demand it. The durable approach: treat multiple deserialization entry points as an architectural smell that triggers review, and audit for other paths that predate the guard's existence — CVE genealogies for this flaw consistently show additional bypasses surface later.