# Verification Report: CVE-2026-49297

## Fix Summary

CVE-2026-49297 is a path traversal vulnerability in Apache Airflow's `GCSToSFTPOperator`. In vulnerable versions, attacker-controlled GCS object names returned by the bucket listing API are joined directly to the SFTP `destination_path` without containment checks. Object names containing `..` or absolute prefixes can therefore write outside the intended destination directory. The official `apache-airflow-providers-google==22.2.1` release adds a lexical containment check, but a variant analysis confirmed that this check is still bypassed when the destination tree contains a symlink that points outside the configured base.

This patch applies a stronger fix to `airflow/providers/google/cloud/transfers/gcs_to_sftp.py`: it preserves the lexical `..`/absolute-path containment check from the 22.2.1 release and adds a best-effort, SFTP-side symlink detection step that rejects any existing symlink component under `destination_path` before the final file write. A new `allow_destination_symlinks` parameter (default `False`) lets operators opt out when the destination tree is known to contain trusted symlinks.

## Changes Made

- `airflow/providers/google/cloud/transfers/gcs_to_sftp.py`
  - Added `import stat`.
  - Added `allow_destination_symlinks: bool = False` to `GCSToSFTPOperator.__init__` and documented it in the class docstring.
  - Hardened `_resolve_destination_path()` with a lexical containment check (identical to the 22.2.1 fix): it normalizes the joined path and raises `ValueError` if the result escapes `destination_path` via `..` segments, an absolute prefix, or any other lexical prefix mismatch.
  - Added `_reject_destination_symlinks()`, which uses the SFTP connection's `lstat()` to walk the resolved path and raise `ValueError` if any existing component under `destination_path` is a symlink.
  - Called `_reject_destination_symlinks()` in `_copy_single_object()` after `create_directory()` and before `store_file()` when `allow_destination_symlinks` is `False`.

## Verification Steps

1. `bundle/coding/verify_fix.sh` was executed. It reads the prepared project cache, restores the vulnerable `apache-airflow-providers-google==22.1.0` source from the canonical wheel extraction, applies `bundle/coding/proposed_fix.diff`, and runs three live end-to-end tests:
   - The original CVE repro harness using a real `fake-gcs-server` GCS JSON API and a real Paramiko SFTP server.
   - The symlink-bypass variant harness (also using real GCS/SFTP protocol endpoints).
   - A regression test that confirms `allow_destination_symlinks=True` still permits writes through trusted symlinks.
2. After the tests, the script restores the original provider source so the project cache is not left modified.

## Test Results

All three verification checks passed:

1. **Original CVE (`..` traversal):** The malicious GCS object `subdir/../../escaped_1.txt` was rejected with a `ValueError` before any SFTP write. The output JSON recorded `escaped_file_exists: false`, `exception_type: "ValueError"`, and `sftp_operations: []`.
2. **Symlink bypass variant:** The non-`..` object `subdir/link/symlink_escape_1.txt` targeting a symlink inside the destination tree was rejected with a `ValueError` before the file was written. The output JSON recorded `escaped_file_exists: false`, `exception_type: "ValueError"`, and `object_contains_dotdot: false`.
3. **Opt-out regression:** With `allow_destination_symlinks=True`, a legitimate write through an existing symlink succeeded and the expected file content was written.

## Remaining Concerns

- The symlink check is best-effort and relies on the SFTP server's `lstat` support. If the remote SFTP server does not report symlink attributes accurately, the check may not detect a symlink.
- The check validates only components under the configured `destination_path`; it does not prevent a DAG author from configuring `destination_path` itself as a symlink to an arbitrary location. That is outside the stated threat model because `destination_path` is a trusted DAG-author configuration.
- Operators that legitimately need symlink support can set `allow_destination_symlinks=True`; deployments should only enable this when the destination tree is fully trusted.
- `GCSTimeSpanFileTransformOperator` already uses a local `Path.resolve().is_relative_to()` check for its worker-local temporary downloads and was not modified by this patch.
