# REPRO-2026-00257: Apache Airflow Google provider path traversal via GCS object names ## Summary Status: published Severity: medium Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00257 CVE: CVE-2026-49297 ## Package Name: apache-airflow-providers-google Ecosystem: pip Affected: before 22.2.1 (both bugs present in 22.0.0; GCSTimeSpanFileTransformOperator fixed in 22.1.0 via PR #67509; GCSToSFTPOperator fixed in 22.2.0 via PR #67667) Fixed: 22.2.1 ## Root Cause ## Summary CVE-2026-49297 is a path traversal vulnerability in Apache Airflow's Google provider transfer operators, demonstrated here through `GCSToSFTPOperator`. When the operator processes a wildcard `source_object`, it obtains object names from the GCS bucket listing API and joins each returned object name directly to the configured SFTP `destination_path`. In vulnerable provider version 22.1.0, a GCS object name such as `subdir/../../escaped_1.txt` is accepted and copied to the SFTP server as `destination_path/subdir/../../escaped_1.txt`, which normalizes outside the intended destination directory. Fixed version 22.2.1 rejects the same object name before any SFTP write. ## Impact - **Package/component affected:** `apache-airflow-providers-google`, specifically `airflow.providers.google.cloud.transfers.gcs_to_sftp.GCSToSFTPOperator` and the same object-name handling pattern described for related GCS transfer operators. - **Affected versions:** Reproduced in `apache-airflow-providers-google==22.1.0`; the ticket states the issue is fixed in `22.2.1`. - **Risk level and consequences:** Medium. A principal who can write object names into a source GCS bucket consumed by an Airflow DAG can cause the DAG to write outside the configured SFTP destination directory. This can overwrite or create unintended files on the SFTP target host with the permissions available to the Airflow/SFTP workflow. ## Impact Parity - **Disclosed/claimed maximum impact:** Arbitrary file overwrite/path traversal through attacker-controlled GCS object names returned by the bucket listing API. - **Reproduced impact from this run:** Full product-path file write outside the configured SFTP `destination_path` using a real Airflow DAG task invocation, real `GCSHook.list/download` calls against a local GCS JSON API emulator, and a real SFTP protocol server. - **Parity:** `full` - **Not demonstrated:** The proof stops at creation of an attacker-controlled file outside `destination_path`; it does not chain the write into code execution or privilege escalation, which were not required by the ticket. ## Root Cause In vulnerable `GCSToSFTPOperator`, wildcard processing calls `GCSHook.list()` and iterates over object names returned by the GCS API. For each object, `_resolve_destination_path()` returns `os.path.join(self.destination_path, source_object)` without canonicalizing the result or checking that it remains inside `destination_path`. GCS object names are attacker-controlled strings for anyone who can write to the source bucket, and object names may contain `..` path segments. Therefore a listed object such as `subdir/../../escaped_1.txt` is transformed into a remote SFTP path under `destination_path/subdir/../../escaped_1.txt`, which normalizes to a sibling of `destination_path`. The fixed provider release `22.2.1` adds a containment check in `_resolve_destination_path()`: it normalizes the joined destination and rejects paths that escape the configured base directory, raising `ValueError` before `SFTPHook.store_file()` is called. The exact upstream fix commit was not provided in the ticket; this reproduction uses the fixed release named by the ticket (`apache-airflow-providers-google==22.2.1`) as the negative control. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. 2. The script uses the prepared project cache when available, ensures two Python environments for `apache-airflow-providers-google==22.1.0` and `==22.2.1`, and downloads/reuses `fsouza/fake-gcs-server` as a local GCS JSON API endpoint. 3. For each attempt, `bundle/repro/product_harness.py`: - starts `fake-gcs-server` on localhost, - creates a real GCS bucket/object via `google-cloud-storage`, with object name `subdir/../../escaped_N.txt`, - starts a Paramiko SFTP server on localhost, - writes a real Airflow DAG containing `GCSToSFTPOperator`, and - invokes it through `airflow tasks test cve_2026_49297_repro_dag copy_malicious_gcs_object_to_sftp`. 4. The script runs two vulnerable attempts and two fixed attempts. Expected evidence: - vulnerable 22.1.0 attempts create `sftp_root/escaped_N.txt` outside `sftp_root/inbox`, - fixed 22.2.1 attempts raise `ValueError: Refusing to copy GCS object ... escapes configured destination_path`, and - fixed attempts perform no SFTP file write. ## Evidence Key runtime artifacts generated by the script: - Main proof log: `bundle/logs/reproduction_steps.log` - Vulnerable attempt summaries: - `bundle/logs/attempt_22.1.0_1.json` - `bundle/logs/attempt_22.1.0_2.json` - Fixed attempt summaries: - `bundle/logs/attempt_22.2.1_1.json` - `bundle/logs/attempt_22.2.1_2.json` - Per-attempt GCS API traces: - `bundle/artifacts/product_test_22.1.0_1/gcs_api_trace.json` - `bundle/artifacts/product_test_22.1.0_2/gcs_api_trace.json` - `bundle/artifacts/product_test_22.2.1_1/gcs_api_trace.json` - `bundle/artifacts/product_test_22.2.1_2/gcs_api_trace.json` - Per-attempt Airflow task logs: - `bundle/artifacts/product_test_22.1.0_1/airflow_tasks_test.log` - `bundle/artifacts/product_test_22.1.0_2/airflow_tasks_test.log` - `bundle/artifacts/product_test_22.2.1_1/airflow_tasks_test.log` - `bundle/artifacts/product_test_22.2.1_2/airflow_tasks_test.log` - Structured verdict: `bundle/repro/validation_verdict.json` - Runtime manifest: `bundle/repro/runtime_manifest.json` Representative evidence from the latest successful run: - The vulnerable Airflow task log shows the real operator processing the malicious object and resolving an escaping SFTP path: `Executing copy of gs://malicious-bucket/subdir/../../escaped_1.txt to .../sftp_root/inbox/subdir/../../escaped_1.txt` - The vulnerable attempt JSON records the out-of-directory write: `"escaped_file_exists": true`, `"file_inside_destination": false`, and an SFTP `open` operation whose remote path is `.../inbox/subdir/../../escaped_1.txt` while the local file path is `.../sftp_root/escaped_1.txt`. - The fixed Airflow task log records the negative control: `ValueError: Refusing to copy GCS object 'subdir/../../escaped_1.txt': resolved destination '.../sftp_root/escaped_1.txt' escapes configured destination_path '.../sftp_root/inbox'.` - The fixed attempt JSON records no escaped file and no SFTP write operations: `"escaped_file_exists": false`, `"exception_type": "ValueError"`, and `"sftp_operations": []`. Environment details captured by the script include exact installed packages and source file paths for the operator and hook, for example: - `apache-airflow==3.2.2` - `apache-airflow-providers-google==22.1.0` for the vulnerable run - `apache-airflow-providers-google==22.2.1` for the fixed run - `apache-airflow-providers-sftp==5.8.2` ## Recommendations / Next Steps - Upgrade `apache-airflow-providers-google` to `22.2.1` or later. - Keep the fixed containment check in `_resolve_destination_path()` and apply equivalent checks to any other operator that maps GCS object names to local or remote filesystem paths. - Add regression tests that use GCS object names containing `..`, absolute-looking path components, and mixed directory separators where applicable. - Prefer containment checks based on normalized/canonical paths and reject any resolved path outside the configured destination root before creating directories or opening remote files. ## Additional Notes - The final reproduction script was run successfully end-to-end and produced fresh current-run artifacts. - The proof uses a local GCS JSON API emulator rather than monkey-patching `GCSHook`; Airflow's real `GCSHook.list()` and `GCSHook.download()` cross an HTTP API boundary. - The proof uses a real Paramiko SFTP protocol server and records accepted SFTP connections and file-open operations. - The script is idempotent: each attempt uses a clean per-attempt artifact directory, a new GCS emulator instance, and a new SFTP server instance. ## Reproduction Details Reproduced: 2026-07-06T10:48:27.290Z Duration: 3415 seconds Tool calls: 352 Turns: Unknown Handoffs: 4 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00257 pruva-verify CVE-2026-49297 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00257&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00257/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-49297 - Source: https://github.com/apache/airflow ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 14232 bytes) - bundle/repro/rca_report.md (analysis, 7914 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 13340 bytes) - bundle/vuln_variant/rca_report.md (analysis, 10610 bytes) - bundle/coding/proposed_fix.diff (patch, 6499 bytes) - bundle/artifact_promotion_manifest.json (other, 16308 bytes) - bundle/artifact_promotion_report.json (other, 16326 bytes) - bundle/vuln_variant/source_identity.json (other, 852 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1070 bytes) - bundle/repro/validation_verdict.json (other, 903 bytes) - bundle/repro/runtime_manifest.json (other, 2036 bytes) - bundle/logs/reproduction_steps.log (log, 29081 bytes) - bundle/logs/attempt_22.1.0_1.json (other, 6078 bytes) - bundle/logs/attempt_22.2.1_1.json (other, 7554 bytes) - bundle/logs/attempt_22.1.0_2.json (other, 6078 bytes) - bundle/logs/attempt_22.2.1_2.json (other, 7554 bytes) - bundle/vuln_variant/symlink_variant_harness.py (script, 14895 bytes) - bundle/logs/vuln_variant_reproduction_steps.log (log, 9348 bytes) - bundle/logs/vuln_variant_attempt_22.2.1_1.json (other, 2206 bytes) - bundle/vuln_variant/variant_manifest.json (other, 3719 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6848 bytes) - bundle/vuln_variant/validation_verdict.json (other, 1433 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 824 bytes) - bundle/logs/vuln_variant_attempt_22.1.0_1.json (other, 2206 bytes) - bundle/coding/verify_cve.json (other, 7470 bytes) - bundle/coding/verify_variant.json (other, 1752 bytes) - bundle/coding/summary_report.md (documentation, 4453 bytes) - bundle/coding/verify_fix.sh (other, 3776 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00257 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00257/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00257 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev