# Patch Analysis: GHSA-2943-9672-r45w Variant

## What the fix changes

The parent vulnerability was reproduced on Airflow 3.2.2 with a serialized DAG field containing `__type="base_trigger"`. In Airflow 3.2.2, `airflow.serialization.serialized_objects.BaseSerialization.deserialize()` has a `DAT.BASE_TRIGGER` case:

```python
elif type_ == DAT.BASE_TRIGGER:
    tr_cls_name, kwargs = cls.deserialize(var)
    tr_cls = import_string(tr_cls_name)
    return tr_cls(**kwargs)
```

Airflow 3.3.0 removes this `DAT.BASE_TRIGGER` branch from `BaseSerialization.deserialize()`. As observed in the parent reproduction, a historical `base_trigger` payload loaded via `SerializedDagModel.dag -> DagSerialization.from_dict -> BaseSerialization.deserialize()` no longer imports the attacker-controlled trigger class. The endpoint is still reached, but the parent payload fails closed and no marker is written.

The tested fixed/latest release was `apache-airflow==3.3.0`. PyPI reported 3.3.0 as latest during this run. Git tag resolution recorded in `bundle/logs/vuln_variant/fixed_version.txt`:

- `refs/tags/3.2.2^{}` = `cde4885818be51a6cdcfdf9275e100bf070025de`
- `refs/tags/3.3.0^{}` = `1438ea3587031417cc85d74323235cf087a058fb`

Relevant source anchor in installed packages:

- 3.2.2: `airflow/serialization/serialized_objects.py`, `BaseSerialization.deserialize()`, line 619 in the installed wheel.
- 3.3.0: `airflow/serialization/serialized_objects.py`, `BaseSerialization.deserialize()`, line 615 in the installed wheel.

A captured before/after source excerpt is saved at `bundle/logs/vuln_variant/base_deserialize_diff.txt`.

## Assumptions the fix makes

The effective fix assumes that the dangerous attacker-controlled class-path import reachable from serialized DAG data is the trigger-specific `DAT.BASE_TRIGGER` deserialization path. That assumption is too narrow. The recursive deserializer is generic and accepts many encoded object types inside arbitrary DAG fields. Removing one encoded type does not ensure that no other encoded type can still import attacker-controlled classes.

The fix also appears to assume that historical/legacy serialized DAG payloads are made safe by rejecting the `base_trigger` tag, rather than by enforcing a general rule: serialized DAG data loaded in Scheduler/API Server contexts must not be able to choose Python modules/classes to import.

## Code paths/inputs not covered

Airflow 3.3.0 still contains the following branch in `BaseSerialization.deserialize()`:

```python
elif type_ == DAT.AIRFLOW_EXC_SER or type_ == DAT.BASE_EXC_SER:
    deser = cls.deserialize(var)
    exc_cls_name = deser["exc_cls_name"]
    args = deser["args"]
    kwargs = deser["kwargs"]
    del deser
    if type_ == DAT.AIRFLOW_EXC_SER:
        exc_cls = import_string(exc_cls_name)
    else:
        exc_cls = import_string(f"builtins.{exc_cls_name}")
    return exc_cls(*args, **kwargs)
```

For `DAT.AIRFLOW_EXC_SER`, `exc_cls_name` is attacker-controlled serialized DAG data. No allowlist or namespace restriction is applied before `import_string(exc_cls_name)`. This remains reachable when the serialized object is placed in a field recursively deserialized by `DagSerialization.from_dict`, including `dag.default_args` in the proof.

Variant payload tested:

```json
{
  "__type": "airflow_exc_ser",
  "__var": {
    "__type": "dict",
    "__var": {
      "exc_cls_name": "evil_exception_module.EvilException",
      "args": [],
      "kwargs": {"__type": "dict", "__var": {"origin": "serialized_dag_default_args"}}
    }
  }
}
```

The API-server endpoint `/ui/grid/structure/{dag_id}?offset=0&limit=100` loads the serialized DAG and reaches this remaining import sink in both 3.2.2 and fixed/latest 3.3.0.

## Completeness of the fix

The fix is incomplete. It closes the parent `BASE_TRIGGER` payload but leaves a same-root-cause bypass through `AIRFLOW_EXC_SER`. The fixed/latest 3.3.0 runtime test imported `evil_exception_module.EvilException` in the API Server process and wrote a marker file from the API-server PID.

This is not merely normal DAG-author code execution. Airflow's security model states that DAG authors can execute arbitrary code in workers, the Dag File Processor, and the Triggerer, and that such behavior alone is not a security vulnerability. It also describes Scheduler/API Server isolation as a deployment security boundary and recommends that DAG-author code should not execute there except through selected deployment-manager-controlled entrypoints. The bypass crosses the same boundary as the parent advisory by using serialized DAG deserialization to execute code in the API Server process.

## Behavior before and after the fix

### Parent `BASE_TRIGGER` behavior

- Airflow 3.2.2: the API-server endpoint loads the serialized DAG, `BaseSerialization.deserialize()` reaches `DAT.BASE_TRIGGER`, imports the attacker class, and writes the marker.
- Airflow 3.3.0: the same parent `base_trigger` payload is rejected or fails to deserialize without importing the attacker class. No marker is written.

### Variant `AIRFLOW_EXC_SER` behavior

- Airflow 3.2.2: the API-server endpoint returns HTTP 200, imports `evil_exception_module.EvilException`, and writes `api_server_airflow_exc_marker.txt`.
- Airflow 3.3.0: the same variant path also returns HTTP 200, imports `evil_exception_module.EvilException`, and writes `api_server_airflow_exc_marker.txt`.

Evidence from the fixed/latest 3.3.0 attempt in this stage:

- `bundle/logs/vuln_variant/airflow_exc_3.3.0_variant.json` records `service_started=true`, `healthcheck_passed=true`, `target_reached=true`, `http_status="200"`, and `import_side_effect_observed=true`.
- `bundle/vuln_variant/artifacts/airflow_exc_3.3.0_variant/api_server_airflow_exc_marker.txt` contains `AIRFLOW_EXC_SER import-time code execution in Airflow API server; pid=<pid>`.

## Recommendation

The fix should be generalized. Serialized DAG deserialization in Scheduler/API Server contexts should not import attacker-controlled class paths from any encoded type. Specifically:

1. Remove `AIRFLOW_EXC_SER` class-path imports from serialized DAG loading, or restrict them to a fixed allowlist of safe built-in/Airflow exception classes.
2. Validate the serialized DAG schema before recursive generic object deserialization so unexpected encoded types cannot be placed in arbitrary DAG fields such as `default_args`.
3. Audit all `BaseSerialization.deserialize()` branches and related serde code for `import_string()` or dynamic class instantiation reachable from serialized DAG metadata.
4. Ensure deployment hardening (`allowed_deserialization_classes`, Scheduler/API Server not importing DAG-bundle modules) is defense in depth rather than the only barrier.
