CVE-2026-49297: Verified Repro With Script Download
CVE-2026-49297: Apache Airflow Google provider path traversal via GCS object names
CVE-2026-49297 is verified against apache-airflow-providers-google · pip. Affected versions: 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 in 22.2.1. Vulnerability class: Path Traversal. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00257.
What Is CVE-2026-49297?
CVE-2026-49297 is a medium-severity path traversal (CWE-22) in Apache Airflow's Google provider transfer operators, GCSToSFTPOperator and GCSTimeSpanFileTransformOperator, which can write downloaded blobs outside their configured destination directory. Pruva reproduced it (reproduction REPRO-2026-00257).
CVE-2026-49297 Severity & CVSS Score
CVE-2026-49297 is rated high severity, with a CVSS base score of 8.1 out of 10.
High — serious impact or readily exploitable. Prioritize remediation.
Affected apache-airflow-providers-google Versions
apache-airflow-providers-google · pip versions 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) are affected.
How to Reproduce CVE-2026-49297
pruva-verify REPRO-2026-00257 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00257/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-49297
- reached the target end-to-end
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
GCS object name 'subdir/../../escaped_N.txt' returned by a bucket listing API for source_object 'subdir/*'
- airflow tasks test
- GCSToSFTPOperator.execute()
- GCSHook.list/download over GCS JSON API
- SFTPHook.store_file
Fixed-version bypass of GCSToSFTPOperator path containment: a lexically safe GCS object name under a symlinked subdirectory is accepted by apache-airflow-providers-google 22.2.1 and SFTP resolution writes outside destination_path.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-49297
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.
- Package/component affected:
apache-airflow-providers-google, specificallyairflow.providers.google.cloud.transfers.gcs_to_sftp.GCSToSFTPOperatorand 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 in22.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_pathusing a real Airflow DAG task invocation, realGCSHook.list/downloadcalls 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
- Run
bundle/repro/reproduction_steps.sh. - The script uses the prepared project cache when available, ensures two Python environments for
apache-airflow-providers-google==22.1.0and==22.2.1, and downloads/reusesfsouza/fake-gcs-serveras a local GCS JSON API endpoint. - For each attempt,
bundle/repro/product_harness.py:- starts
fake-gcs-serveron localhost, - creates a real GCS bucket/object via
google-cloud-storage, with object namesubdir/../../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.
- starts
- The script runs two vulnerable attempts and two fixed attempts. Expected evidence:
- vulnerable 22.1.0 attempts create
sftp_root/escaped_N.txtoutsidesftp_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.
- vulnerable 22.1.0 attempts create
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.jsonbundle/logs/attempt_22.1.0_2.json
- Fixed attempt summaries:
bundle/logs/attempt_22.2.1_1.jsonbundle/logs/attempt_22.2.1_2.json
- Per-attempt GCS API traces:
bundle/artifacts/product_test_22.1.0_1/gcs_api_trace.jsonbundle/artifacts/product_test_22.1.0_2/gcs_api_trace.jsonbundle/artifacts/product_test_22.2.1_1/gcs_api_trace.jsonbundle/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.logbundle/artifacts/product_test_22.1.0_2/airflow_tasks_test.logbundle/artifacts/product_test_22.2.1_1/airflow_tasks_test.logbundle/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.txtThe vulnerable attempt JSON records the out-of-directory write:
"escaped_file_exists": true,"file_inside_destination": false, and an SFTPopenoperation whose remote path is.../inbox/subdir/../../escaped_1.txtwhile 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.2apache-airflow-providers-google==22.1.0for the vulnerable runapache-airflow-providers-google==22.2.1for the fixed runapache-airflow-providers-sftp==5.8.2
Recommendations / Next Steps
- Upgrade
apache-airflow-providers-googleto22.2.1or 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 realGCSHook.list()andGCSHook.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.
Variant Analysis & Alternative Triggers for CVE-2026-49297
A distinct fixed-version bypass was confirmed for CVE-2026-49297 in GCSToSFTPOperator. The parent reproduction used attacker-controlled GCS object names containing .. segments. The variant uses an attacker-controlled GCS object name with no .. segment (subdir/link/symlink_escape_1.txt) and a destination tree containing a symlink (destination_path/subdir/link) that points outside destination_path. Apache Airflow Google provider 22.2.1 accepts the path because _resolve_destination_path() performs only lexical normpath prefix containment. The subsequent SFTP write follows the symlink and creates the file outside the configured SFTP destination directory.
Fix Coverage / Assumptions
- The original fix in
apache-airflow-providers-google==22.2.1changesGCSToSFTPOperator._resolve_destination_path()inairflow/providers/google/cloud/transfers/gcs_to_sftp.py. - The fixed code joins
destination_pathand the GCS object name, appliesos.path.normpath(), and rejects paths where the normalized string is outside the normalized base directory. - The invariant assumed by the fix is: if the normalized destination path string begins with the normalized
destination_pathprefix, the SFTP write is contained underdestination_path. - That invariant covers ordinary lexical traversal using
..segments and absolute-path absorption. It does not cover filesystem- or SFTP-server-level path resolution after the lexical check, especially symlinks already present insidedestination_path. - The related
GCSTimeSpanFileTransformOperatorpath inairflow/providers/google/cloud/operators/gcs.pyalready usesPath.resolve().is_relative_to(...)for worker-local temp downloads, so the tested bypass is specific to the SFTP destination path check inGCSToSFTPOperator. - Airflow's security model treats DAG authors as trusted and says arbitrary DAG-author code is not itself a vulnerability. This variant preserves the same trust boundary as the parent claim: a less-trusted principal controls GCS object names returned by the bucket listing API, while the DAG and SFTP destination are deployment-controlled. The additional precondition is that the configured SFTP destination tree contains a symlink to another server-side path.
Variant / Alternate Trigger
Variant type: fixed-version bypass via alternate object path semantics.
Entrypoint: Airflow task/operator execution of
GCSToSFTPOperator.Attacker-controlled input: GCS object name returned by bucket listing:
subdir/link/symlink_escape_1.txt.Important distinction from parent: the object name contains no
..path segment (object_contains_dotdot: falsein the evidence JSON).Server-side setup/precondition: the SFTP destination directory contains
subdir/linkas a symlink tooutside_target, which is outside the configureddestination_path.Code path:
GCSToSFTPOperator.execute()lists GCS objects for wildcardsource_object='subdir/*'.- It calls
_resolve_destination_path(source_object, prefix=...). - In fixed
22.2.1,_resolve_destination_path()performs lexicalos.path.normpath(os.path.join(...))containment and returns the path because it is lexically insidedestination_path. _copy_single_object()downloads the GCS object to a temp file and callsSFTPHook.store_file(destination_path, tmp.name).- The SFTP server resolves
destination_path/subdir/link/symlink_escape_1.txtthrough the symlink and writesoutside_target/symlink_escape_1.txtoutside the intended destination.
Package/component affected:
apache-airflow-providers-google, specificallyairflow.providers.google.cloud.transfers.gcs_to_sftp.GCSToSFTPOperator.Affected versions as tested:
- Vulnerable control:
apache-airflow-providers-google==22.1.0. - Fixed-version bypass target:
apache-airflow-providers-google==22.2.1at Apache Airflow tagproviders-google/22.2.1, commitc7bcb8d40f5fa42a98161fadb2166a0fa7fa5150.
- Vulnerable control:
Risk level and consequences: Medium, with a deployment-dependent precondition. If an Airflow deployment ingests GCS buckets writable by less-trusted principals and the configured SFTP destination contains symlinks to other writable locations, the bucket writer can choose object names that write outside the configured destination path despite the
22.2.1lexical traversal fix.
Impact Parity
- Disclosed/claimed maximum impact for parent: arbitrary file overwrite/path traversal on the SFTP server or worker host through attacker-controlled GCS object names.
- Reproduced impact from this variant run: creation of an attacker-controlled file outside the configured SFTP
destination_pathon the fixed provider version22.2.1. - Parity:
partialtofulldepending on deployment. The file-write primitive outsidedestination_pathis fully reproduced; exploitability additionally requires a symlink inside the destination tree. - Not demonstrated: code execution, privilege escalation, or creation of the symlink by the GCS bucket writer. The proof assumes the symlink already exists in the SFTP destination tree.
Root Cause
The same underlying sink remains reachable because the fixed GCSToSFTPOperator verifies only the lexical path string before handing it to the SFTP server. The parent bug was caused by joining attacker-controlled GCS object names to a destination filesystem path without proving containment at the eventual write sink. The 22.2.1 fix rejects obvious lexical escapes such as subdir/../../escaped.txt, but it still treats a path as safe solely because the normalized string is under destination_path. Real filesystem/SFTP resolution can map that lexically in-bounds path outside the base when an intermediate component is a symlink.
The relevant fixed code is in airflow/providers/google/cloud/transfers/gcs_to_sftp.py:
execute()obtains object names fromgcs_hook.list(...)and calls_resolve_destination_path()._resolve_destination_path()computesresolved = os.path.normpath(os.path.join(self.destination_path, source_object))and checks string-prefix containment._copy_single_object()later callssftp_hook.store_file(destination_path, tmp.name)without verifying the SFTP server's canonical target.
The exact fix commit was not identified from a single patch URL in the ticket, but the tested fixed release tag is providers-google/22.2.1 resolved to commit c7bcb8d40f5fa42a98161fadb2166a0fa7fa5150.
Reproduction Steps
- Run
bundle/vuln_variant/reproduction_steps.sh. - The script ensures/reuses Python environments for provider versions
22.1.0and22.2.1, starts a realfake-gcs-serverJSON API endpoint, starts a real Paramiko SFTP server, creates a destination tree with a symlink insidedestination_path, and executes the realGCSToSFTPOperatordirectly. - Expected evidence:
- The vulnerable version writes outside
destination_paththrough the symlink. - The fixed version
22.2.1also writes outsidedestination_paththrough the symlink. - The fixed attempt JSON records
object_contains_dotdot: false,escaped_file_exists: true,operator_succeeded: true, andfile_inside_destination_by_realpath: false.
- The vulnerable version writes outside
- Exit code semantics: exit
0means the bypass reproduced on the fixed version; exit1means no fixed-version bypass was confirmed. The script was run twice and exited0both times.
Evidence
Primary artifacts:
- Main variant proof log:
bundle/logs/vuln_variant_reproduction_steps.log - Vulnerable control JSON:
bundle/logs/vuln_variant_attempt_22.1.0_1.json - Fixed bypass JSON:
bundle/logs/vuln_variant_attempt_22.2.1_1.json - Fixed source identity:
bundle/logs/vuln_variant_fixed_version.txt - Vulnerable source identity:
bundle/logs/vuln_variant_vulnerable_version.txt - Structured verdict:
bundle/vuln_variant/validation_verdict.json - Source identity:
bundle/vuln_variant/source_identity.json - Runtime manifest:
bundle/vuln_variant/runtime_manifest.json
Representative fixed-version evidence from bundle/logs/vuln_variant_attempt_22.2.1_1.json:
"attacker_controlled_gcs_object": "subdir/link/symlink_escape_1.txt""object_contains_dotdot": false"operator_succeeded": true"escaped_file_exists": true"file_inside_destination_by_realpath": false- The SFTP
openoperation's lexical local path was under.../sftp_root/inbox/subdir/link/symlink_escape_1.txt, while itsrealpathwas.../sftp_root/outside_target/symlink_escape_1.txt.
The main log from both verification runs includes:
vulnerable_exploited=Truefixed_exploited=TrueRESULT: confirmed fixed-version bypass (exit 0)
Environment/source details captured:
apache-airflow==3.2.2apache-airflow-providers-google==22.1.0for the vulnerable controlapache-airflow-providers-google==22.2.1for the fixed targetapache-airflow-providers-sftp==5.8.2- Fixed provider tag commit:
c7bcb8d40f5fa42a98161fadb2166a0fa7fa5150
Recommendations / Next Steps
- Treat SFTP destination containment as a sink-level property, not only a lexical string property.
- If the intended guarantee is strict containment under
destination_path, resolve and reject symlinked intermediate path components before writing. For SFTP this may require walking each path component withlstat/readlinkwhere supported, rejecting symlinks under the configured base, or documenting thatdestination_pathmust not contain symlinks. - Consider adding an option such as
allow_destination_symlinks=Falsedefaulting to the safer behavior for GCS-to-SFTP transfers. - Add regression tests where a lexically safe object name traverses an existing symlink inside the SFTP destination tree.
- Keep the current lexical
../absolute path guard, but extend it with symlink-aware checks or an explicit documented limitation and deployment hardening guidance.
Additional Notes
- The variant reproduction script is idempotent: each run recreates per-version test roots under
bundle/vuln_variant/artifacts/and overwrites stable JSON logs. - The script was executed twice successfully as required; both runs completed without crashing and exited
0because the fixed-version bypass reproduced. - This finding has a narrower precondition than the parent
..traversal: a symlink must exist inside the SFTP destination tree. It is nevertheless a meaningful bypass of the patch's containment assumption because the fixed version accepts and writes a path that resolves outsidedestination_path.
CVE-2026-49297 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-49297
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-49297
Upgrade apache-airflow-providers-google · pip to 22.2.1 or later.
FAQ: CVE-2026-49297
Which apache-airflow-providers-google versions are affected by CVE-2026-49297, and where is it fixed?
How severe is CVE-2026-49297?
Does exploiting CVE-2026-49297 require compromising Airflow directly?
How can I reproduce CVE-2026-49297?
References for CVE-2026-49297
Authoritative sources for CVE-2026-49297 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.