Skip to content

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.

REPRO-2026-00257 apache-airflow-providers-google · pip Path Traversal Variant found Jul 6, 2026 CVE entry ↗ .txt
Severity
HIGH
CVSS
8.1
Confidence
HIGH
Reproduced in
56m 55s
Tool calls
352
Spend
$10.75
01 · Overview

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).

02 · Severity & CVSS

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 threat level
8.1 / 10 CVSS base
Weakness CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

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
or 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
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-49297

Security impact — reproduced
  • reached the target end-to-end
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

GCS object name 'subdir/../../escaped_N.txt' returned by a bucket listing API for source_object 'subdir/*'

Attack chain
  1. airflow tasks test
  2. GCSToSFTPOperator.execute()
  3. GCSHook.list/download over GCS JSON API
  4. SFTPHook.store_file
Variants tested

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 865 events · 352 tool calls · 57 min
57 minDuration
352Tool calls
227Reasoning steps
865Events
2Dead-ends
Agent activity over 57 min
Support
39
Hypothesis
2
Repro
471
Judge
57
Variant
118
Coding
172
0:0056:55

Root Cause and Exploit Chain for CVE-2026-49297

Versions: Reproduced in apache-airflow-providers-google==22.1.0; the ticket states the issue is fixed in 22.2.1.

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, 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.

Variant Analysis & Alternative Triggers for CVE-2026-49297

Versions: versions as tested:Fixed: -version bypass target: apache-airflow-providers-google==22.2.1 at Apache Airflow tag providers-google/22.2.1, commit c7bcb8d40f5fa42a98161fadb2166a0fa7fa5150.

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.1 changes GCSToSFTPOperator._resolve_destination_path() in airflow/providers/google/cloud/transfers/gcs_to_sftp.py.
  • The fixed code joins destination_path and the GCS object name, applies os.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_path prefix, the SFTP write is contained under destination_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 inside destination_path.
  • The related GCSTimeSpanFileTransformOperator path in airflow/providers/google/cloud/operators/gcs.py already uses Path.resolve().is_relative_to(...) for worker-local temp downloads, so the tested bypass is specific to the SFTP destination path check in GCSToSFTPOperator.
  • 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: false in the evidence JSON).

  • Server-side setup/precondition: the SFTP destination directory contains subdir/link as a symlink to outside_target, which is outside the configured destination_path.

  • Code path:

    1. GCSToSFTPOperator.execute() lists GCS objects for wildcard source_object='subdir/*'.
    2. It calls _resolve_destination_path(source_object, prefix=...).
    3. In fixed 22.2.1, _resolve_destination_path() performs lexical os.path.normpath(os.path.join(...)) containment and returns the path because it is lexically inside destination_path.
    4. _copy_single_object() downloads the GCS object to a temp file and calls SFTPHook.store_file(destination_path, tmp.name).
    5. The SFTP server resolves destination_path/subdir/link/symlink_escape_1.txt through the symlink and writes outside_target/symlink_escape_1.txt outside the intended destination.
  • Package/component affected: apache-airflow-providers-google, specifically airflow.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.1 at Apache Airflow tag providers-google/22.2.1, commit c7bcb8d40f5fa42a98161fadb2166a0fa7fa5150.
  • 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.1 lexical 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_path on the fixed provider version 22.2.1.
  • Parity: partial to full depending on deployment. The file-write primitive outside destination_path is 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 from gcs_hook.list(...) and calls _resolve_destination_path().
  • _resolve_destination_path() computes resolved = os.path.normpath(os.path.join(self.destination_path, source_object)) and checks string-prefix containment.
  • _copy_single_object() later calls sftp_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

  1. Run bundle/vuln_variant/reproduction_steps.sh.
  2. The script ensures/reuses Python environments for provider versions 22.1.0 and 22.2.1, starts a real fake-gcs-server JSON API endpoint, starts a real Paramiko SFTP server, creates a destination tree with a symlink inside destination_path, and executes the real GCSToSFTPOperator directly.
  3. Expected evidence:
    • The vulnerable version writes outside destination_path through the symlink.
    • The fixed version 22.2.1 also writes outside destination_path through the symlink.
    • The fixed attempt JSON records object_contains_dotdot: false, escaped_file_exists: true, operator_succeeded: true, and file_inside_destination_by_realpath: false.
  4. Exit code semantics: exit 0 means the bypass reproduced on the fixed version; exit 1 means no fixed-version bypass was confirmed. The script was run twice and exited 0 both 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 open operation's lexical local path was under .../sftp_root/inbox/subdir/link/symlink_escape_1.txt, while its realpath was .../sftp_root/outside_target/symlink_escape_1.txt.

The main log from both verification runs includes:

  • vulnerable_exploited=True
  • fixed_exploited=True
  • RESULT: confirmed fixed-version bypass (exit 0)

Environment/source details captured:

  • apache-airflow==3.2.2
  • apache-airflow-providers-google==22.1.0 for the vulnerable control
  • apache-airflow-providers-google==22.2.1 for the fixed target
  • apache-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 with lstat/readlink where supported, rejecting symlinks under the configured base, or documenting that destination_path must not contain symlinks.
  • Consider adding an option such as allow_destination_symlinks=False defaulting 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 0 because 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 outside destination_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.

Event 1/40
0:001:22
0:00
session startedaccounts/fireworks/models/kimi-k2p7-code · CVE-2026-49297 · REPRO-20
0:03
0:05
web search
0:08
0:09
0:11
0:13
0:14
web search
0:16
0:17
0:20
0:21
web search
0:22
0:26
0:27
web search
0:30
0:32
0:35
web search
0:44
0:45
0:50
0:54
1:04
1:05
web search
1:20
1:21
extract_facts
no facts extracted
1:22
1:22
08 · How to Fix

How to Fix CVE-2026-49297

Upgrade apache-airflow-providers-google · pip to 22.2.1 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-49297 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-49297

Which apache-airflow-providers-google versions are affected by CVE-2026-49297, and where is it fixed?

Versions before 22.2.1 are affected (both operators had the bug present in 22.0.0; GCSTimeSpanFileTransformOperator was fixed in 22.1.0 via PR #67509, and GCSToSFTPOperator was fixed in 22.2.0 via PR #67667). It is fully fixed in apache-airflow-providers-google 22.2.1.

How severe is CVE-2026-49297?

It is rated medium severity: a principal who can write object names into a source GCS bucket consumed by an Airflow DAG can cause the DAG to write or overwrite files outside the configured destination directory on the SFTP target or worker host.

Does exploiting CVE-2026-49297 require compromising Airflow directly?

No — the attacker needs write access to the source GCS bucket that an Airflow DAG ingests from, not access to Airflow itself. This is why deployments ingesting from buckets writable by less-trusted principals are exposed.

How can I reproduce CVE-2026-49297?

Download the verified script from this page and run it in an isolated environment against apache-airflow-providers-google==22.1.0. Create a GCS object with a traversal name such as subdir/../../escaped_1.txt in a bucket used by a wildcard GCSToSFTPOperator source_object, confirm the file lands outside the configured SFTP destination_path, and confirm the fixed 22.2.1 provider rejects the same object name before any SFTP write.
11 · References

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.