criticalCVE-2026-46428Jul 28, 2026

CVE-2026-46428: lettre Inverted Boolean Disables TLS Hostname Verification (boring-tls)

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A flipped boolean in the lettre Rust email library silently turns off TLS hostname verification for every app using the boring-tls backend with default-strict settings, letting an on-path attacker…

Packagelettre
Ecosystemrust
Affected>= 0.10.1, < 0.11.22
Fixed in0.11.22
CVE-2026-46428: lettre Inverted Boolean Disables TLS Hostname Verification (boring-tls)

The problem

lettre's TlsParametersBuilder exposes accept_invalid_hostnames: bool, where true means skip verification and false means enforce it. The boring-tls integration passes this flag directly to boring's set_verify_hostname() / verify_hostname(), whose semantics are the exact opposite: true enforces, false skips.

The result is that every caller using the safe default (accept_invalid_hostnames = false) gets hostname verification silently disabled. An on-path attacker holding any chain-valid certificate (a free Let's Encrypt cert works) can intercept the full SMTP session, including AUTH credentials, message bodies, and envelope data.

The native-tls and rustls backends are not affected.

Proof of concept

A working proof-of-concept for CVE-2026-46428 in lettre, with the exact payload below.

rust
// Cargo.toml: lettre = { version = ">=0.10.1,<0.11.22", features = ["boring-tls"] }
// Default-strict config -- looks safe, but hostname verification is OFF under boring-tls.

use lettre::transport::smtp::client::{Tls, TlsParameters};
use lettre::{AsyncSmtpTransport, Tokio1Executor, Message, Transport};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // accept_invalid_hostnames defaults to false ("strict") --
    // but boring receives verify_hostname(false), so verification is SKIPPED.
    let tls = TlsParameters::builder("mail.example.com".to_owned())
        .build_boring()
        .unwrap();

    let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay("mail.example.com")?
        .tls(Tls::Required(tls))
        .build();

    // Attacker on the network path presents a cert for attacker.example
    // (signed by any trusted CA). boring accepts the handshake because
    // verify_hostname was called with false. lettre completes the SMTP
    // transaction against the attacker, leaking credentials and message.
    let message = Message::builder()
        .from("sender@example.com".parse()?)
        .to("recipient@example.com".parse()?)
        .subject("Test")
        .body("Body".to_string())?;

    mailer.send(message).await?; // succeeds against attacker SMTP server
    Ok(())
}

The root cause is a semantic inversion at two boring call sites (CWE-297: Improper Validation of Certificate with Host Mismatch). In net.rs (sync) the code called .verify_hostname(*accept_invalid_hostnames), and in async_net.rs it called config.set_verify_hostname(accept_invalid_hostnames).

Because boring's flag means "do verify" (true = enforce), passing false (the strict default from lettre's perspective) told boring to skip verification entirely.

The patch commit f5efffc negates the flag at both sites: .verify_hostname(!*accept_invalid_hostnames) and config.set_verify_hostname(!accept_invalid_hostnames). This one-character fix corrects the inversion. The bug was introduced in PR #797 (commit 985fa7e) and was never caught because no test exercised accept_invalid_hostnames end-to-end with the boring backend.

The fix

Upgrade to lettre 0.11.22 or later. The fix is commit f5efffc88360dbdbfcef80f465e42d5bce68ca35, which negates the boolean at both TLS-upgrade call sites in the boring backend. If you cannot upgrade immediately, avoid the boring-tls feature and use native-tls or rustls instead, both of which handle the flag correctly.

Reported by Cloudflare Email Forwarding Team.

References: [1][2][3][4][5][6]

Related research