## Summary

A distinct bypass was confirmed against the fixed/latest Apache Airflow 3.3.0 release. The parent reproduction used serialized DAG data with `__type="base_trigger"`; Airflow 3.3.0 removes that `DAT.BASE_TRIGGER` deserialization branch. However, `BaseSerialization.deserialize()` still accepts `__type="airflow_exc_ser"` and still calls `import_string()` on attacker-controlled `exc_cls_name`. By placing an `AIRFLOW_EXC_SER` object in the serialized DAG's `dag.default_args` field and loading the DAG through the real API-server UI endpoint, the fixed Airflow 3.3.0 API server imported an attacker-controlled module and executed import-time Python code.

## Fix Coverage / Assumptions

The original fix appears to rely on the invariant that removing the historical `BASE_TRIGGER` case closes the dangerous serialized-DAG class-path import path. The fix explicitly covers the parent trigger path:

- Parent path in 3.2.2: `SerializedDagModel.dag -> DagSerialization.from_dict -> BaseSerialization.deserialize() -> DAT.BASE_TRIGGER -> import_string(trigger_class_path)`.
- Fixed behavior in 3.3.0: `DAT.BASE_TRIGGER` is no longer handled by `BaseSerialization.deserialize()`, so the parent `base_trigger` payload fails closed before importing the attacker module.

The fix does **not** cover all class-path imports reachable from serialized DAG fields. In both Airflow 3.2.2 and 3.3.0, `BaseSerialization.deserialize()` keeps this branch:

- `DAT.AIRFLOW_EXC_SER` / `DAT.BASE_EXC_SER` deserializes a dict containing `exc_cls_name`, `args`, and `kwargs`.
- For `DAT.AIRFLOW_EXC_SER`, it calls `import_string(exc_cls_name)` directly.
- No allowlist, namespace check, or fixed exception-class map is applied before importing.

The target's documented security model was reviewed. Airflow states that DAG authors can execute arbitrary code on workers, the Dag File Processor, and the Triggerer; such execution alone is expected behavior and not a vulnerability. The same security model separately says deployment managers may isolate Scheduler/API Server from DAG-author code and that no DAG-author provided code should execute in those processes except through selected entrypoint mechanisms. The variant uses the same security boundary as the parent advisory: attacker-controlled serialized DAG data causes import-time code execution inside the API Server process, not merely inside a task, Dag File Processor, or Triggerer.

## Variant / Alternate Trigger

The bypass uses a different serialized object type and field path from the parent:

- Parent trigger: `dag.default_args = {"__type": "base_trigger", "__var": ["evil_trigger_module.EvilTrigger", ...]}`.
- Variant trigger: `dag.default_args = {"__type": "airflow_exc_ser", "__var": {"__type": "dict", "__var": {"exc_cls_name": "evil_exception_module.EvilException", "args": [], "kwargs": ...}}}`.

Exact runtime entry point:

- HTTP endpoint: `GET /ui/grid/structure/{dag_id}?offset=0&limit=100` on `airflow api-server`.
- Product path: API server route loads `SerializedDagModel.dag`, `DagSerialization.from_dict` walks serialized DAG fields, and `BaseSerialization.deserialize()` processes `AIRFLOW_EXC_SER` in `dag.default_args`.
- Sink: `airflow/serialization/serialized_objects.py`, `BaseSerialization.deserialize()`, `DAT.AIRFLOW_EXC_SER` branch, `import_string(exc_cls_name)`.

The variant is materially distinct because it does not use the removed `BASE_TRIGGER` branch. It uses the still-present exception-serialization branch in fixed 3.3.0.

## Impact

- Package/component affected: `apache-airflow` / `apache-airflow-core`, serialized DAG deserialization in `airflow.serialization.serialized_objects.BaseSerialization` as reached by the API Server.
- Affected versions tested: Airflow 3.2.2 and fixed/latest Airflow 3.3.0. PyPI reported `3.3.0` as both the installed fixed version and latest available release during this run.
- Risk level and consequences: the same security-boundary violation as the parent. A DAG author or actor who can cause malicious serialized DAG data to be loaded can execute Python code in the API Server process. This may expose API-server process privileges, local files, secrets, configuration, and metadata-database access available to that process.

## Impact Parity

- Disclosed/claimed maximum impact for the parent: code execution in Scheduler/API Server through unrestricted class-path import during serialized DAG deserialization.
- Reproduced impact from this variant run: code execution in the real Airflow API Server process on fixed/latest Airflow 3.3.0. The imported module writes `api_server_airflow_exc_marker.txt` at import time and records the API-server PID.
- Parity: `full`.
- Not demonstrated: the proof writes a marker file only. It does not run a shell command, persist a backdoor, exfiltrate secrets, or attempt post-exploitation.

## Root Cause

The same underlying root cause remains: `BaseSerialization.deserialize()` treats serialized DAG data as trusted enough to control Python import paths. Airflow 3.3.0 removes the `BASE_TRIGGER` import sink, but leaves the `AIRFLOW_EXC_SER` import sink reachable from arbitrary serialized fields in a DAG object. When `dag.default_args` contains an `AIRFLOW_EXC_SER` object, the recursive deserializer reaches:

```python
elif type_ == DAT.AIRFLOW_EXC_SER or type_ == DAT.BASE_EXC_SER:
    deser = cls.deserialize(var)
    exc_cls_name = deser["exc_cls_name"]
    ...
    if type_ == DAT.AIRFLOW_EXC_SER:
        exc_cls = import_string(exc_cls_name)
```

The tested fixed tag resolution for Airflow 3.3.0 was `refs/tags/3.3.0^{}` = `1438ea3587031417cc85d74323235cf087a058fb`. The vulnerable comparison tag resolution for Airflow 3.2.2 was `refs/tags/3.2.2^{}` = `cde4885818be51a6cdcfdf9275e100bf070025de`. Known advisory fix references are GitHub PRs `apache/airflow#66002` and `apache/airflow#68528`; the local package comparison shows the relevant effective change for the parent path is removal of the `DAT.BASE_TRIGGER` branch from `BaseSerialization.deserialize()`.

## Reproduction Steps

1. Run `bundle/vuln_variant/reproduction_steps.sh`.
2. The script:
   - Reuses prepared Airflow virtualenvs when available, or installs `apache-airflow==3.2.2` and `apache-airflow==3.3.0` into stage-local virtualenvs.
   - Creates a fresh Airflow home and SQLite metadata DB for each version.
   - Places `evil_exception_module.py` in the DAG folder; the module writes a marker file at import time.
   - Seeds a serialized DAG row containing an `AIRFLOW_EXC_SER` object in `dag.default_args` with `exc_cls_name="evil_exception_module.EvilException"`.
   - Starts the real `airflow api-server`, waits for `/api/v2/monitor/health`, then requests `/ui/grid/structure/{dag_id}?offset=0&limit=100`.
   - Runs the same variant against vulnerable 3.2.2 and fixed/latest 3.3.0 side by side.
3. Expected evidence:
   - Both versions return HTTP 200 from the UI endpoint.
   - Both versions create `api_server_airflow_exc_marker.txt` containing `AIRFLOW_EXC_SER import-time code execution in Airflow API server; pid=<api-server-pid>`.
   - The script exits 0 only when the fixed/latest version also reproduces the import-time execution, i.e. a bypass is confirmed.

## Evidence

Primary evidence files:

- `bundle/logs/vuln_variant/reproduction_steps.log` — full output from the stage-specific reproduction script. The script was executed twice successfully and confirmed idempotency.
- `bundle/logs/vuln_variant/airflow_exc_3.2.2_variant.json` — vulnerable-version attempt.
- `bundle/logs/vuln_variant/airflow_exc_3.3.0_variant.json` — fixed/latest-version attempt.
- `bundle/vuln_variant/artifacts/airflow_exc_3.2.2_variant/api_server_airflow_exc_marker.txt` — vulnerable API-server marker.
- `bundle/vuln_variant/artifacts/airflow_exc_3.3.0_variant/api_server_airflow_exc_marker.txt` — fixed/latest API-server marker.
- `bundle/logs/vuln_variant/base_deserialize_diff.txt` — captured source excerpts showing 3.3.0 removed `BASE_TRIGGER` but retained `AIRFLOW_EXC_SER` import.
- `bundle/logs/vuln_variant/fixed_version.txt` and `bundle/logs/vuln_variant/latest_version.txt` — tested release identity and PyPI/latest checks.
- `bundle/vuln_variant/runtime_manifest.json` — structured runtime evidence manifest.

Key evidence from the verified second run:

- Airflow 3.3.0 attempt JSON: `service_started=true`, `healthcheck_passed=true`, `target_reached=true`, `http_status="200"`, and `import_side_effect_observed=true`.
- Fixed/latest marker content: `AIRFLOW_EXC_SER import-time code execution in Airflow API server; pid=17602`.
- API-server access log tail in the 3.3.0 attempt shows `method=GET path=/ui/grid/structure/ghsa_2943_airflow_exc_variant_3_3_0 ... status_code=200`.
- Script verdict: `RESULT: CONFIRMED BYPASS - Airflow 3.3.0 still imports attacker-controlled AIRFLOW_EXC_SER exc_cls_name during serialized DAG deserialization.`

## Recommendations / Next Steps

- Treat serialized DAG deserialization as untrusted input whenever it is loaded in Scheduler/API Server contexts.
- Extend the fix beyond `BASE_TRIGGER`: remove or constrain the `AIRFLOW_EXC_SER` class-path import path.
- If exception deserialization must remain, use a fixed map of built-in/Airflow exception classes or a strict allowlist that cannot include DAG-bundle modules by default.
- Apply the same policy consistently to all `BaseSerialization.deserialize()` branches that can import or instantiate classes from serialized data.
- Consider rejecting `AIRFLOW_EXC_SER` in DAG fields where exception objects are not semantically expected, such as `dag.default_args`, or validating serialized DAG schemas before recursive object deserialization.
- As defense in depth, deployments should keep Scheduler/API Server unable to import DAG-author-controlled modules and set `[core] allowed_deserialization_classes` narrowly, but the code fix should not rely on deployment hardening alone.

## Additional Notes

The reproduction script was run twice in this stage and completed successfully both times with exit code 0, confirming idempotency. The proof uses a local API-server instance, SQLite metadata database, and localhost HTTP requests. The exploit precondition matches the parent reproduction: the attacker-controlled class path resolves in the API Server process. The test does not claim that ordinary DAG author code execution in workers, Dag File Processor, or Triggerer is itself a vulnerability; the finding is specifically code execution in API Server during serialized DAG deserialization.
