# lettre has TLS hostname verification disabled when using Boring TLS backend

Inverted boolean in lettre's boring-tls integration disables TLS hostname verification under default-strict config, letting an on-path attacker with any valid cert intercept SMTP credentials and mail.

- Payout: Research
- Severity: critical
- Program: lettre
- Found by: edevil
- Disclosed: 2026-07-28
- CVE: CVE-2026-46428
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4pj9-g833-qx53
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4pj9-g833-qx53
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:email tech:rust technique:source-review vuln:crypto vuln:misconfiguration

**Target:** SMTP client TLS layer (boring-tls backend)
**Root cause:** lettre's `accept_invalid_hostnames` flag (true = skip verification) is passed directly to boring's `set_verify_hostname` (true = enforce), inverting semantics at both sync and async TLS-upgrade sites.
**Steps:**
1. Build lettre >=0.10.1 with `boring-tls` feature, default strict config.
2. Obtain any chain-valid cert for an attacker-controlled hostname (e.g. Let's Encrypt for `attacker.example`).
3. Place attacker SMTP server on path to intended MX; present `attacker.example` cert on STARTTLS/implicit TLS.
4. Client connects via `TlsParameters::builder("mail.example.com").build_boring()` with `Tls::Required`; handshake succeeds despite SAN mismatch.
5. lettre sends MAIL FROM, RCPT TO, body, and SMTP AUTH credentials to attacker.
6. Inverse check: with `.dangerous_accept_invalid_hostnames(true)`, mismatched-SAN server is rejected, proving inversion.
**How found:** Code review of the boring-tls integration comparing lettre's `accept_invalid_hostnames` flag semantics against boring's `set_verify_hostname` API docs, spotting the opposite polarity at both call sites; noted zero end-to-end tests cover the flag.
**Exploitation:** On-path attacker with any CA-signed cert for a domain they own intercepts the SMTP submission; boring accepts the handshake, lettre proceeds, attacker reads/writes envelope, message bodies, attachments, headers, SMTP AUTH credentials (PLAIN/LOGIN/CRAM-MD5), and can drop DKIM signatures.
**Insight:** When wrapping a TLS library, audit flag polarity against the upstream API docs — libraries with opposite-named booleans (`accept_invalid_*` vs `verify_*`) are a classic inversion trap, and missing end-to-end tests for the 'dangerous' flag hide it.

## Proof of concept (copied exactly from the original report)

**Rust PoC using default-strict TLS to attacker**

```
use lettre::transport::smtp::client::{Tls, TlsParameters};
       use lettre::{AsyncSmtpTransport, Tokio1Executor, Message};
       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();
       mailer.send(message).await?;     // succeeds against attacker
```

