Rust reqwest Accept Invalid Certificates Vulnerability

High Risk Insecure Transport
RustreqwestTLSCertificate ValidationMITMTransport Security

What it is

Application configures reqwest HTTP client to accept invalid TLS certificates, making connections vulnerable to man-in-the-middle attacks and certificate spoofing.

use reqwest;
use tokio;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // Vulnerable: Accept invalid certificates
    let client = reqwest::Client::builder()
        .danger_accept_invalid_certs(true)  // DANGEROUS
        .build()?;
    
    let response = client
        .get("https://api.example.com/data")
        .send()
        .await?;
    
    println!("Response: {}", response.text().await?);
    Ok(())
}

// Vulnerable: Global client with disabled verification
lazy_static! {
    static ref HTTP_CLIENT: reqwest::Client = reqwest::Client::builder()
        .danger_accept_invalid_certs(true)  // All requests vulnerable
        .build()
        .unwrap();
}

pub async fn api_call(url: &str) -> Result<String, reqwest::Error> {
    // Uses insecure global client
    let response = HTTP_CLIENT.get(url).send().await?;
    response.text().await
}

pub async fn webhook_call(url: &str, data: &str) -> Result<(), reqwest::Error> {
    // Vulnerable: Ad-hoc client with disabled verification
    let client = reqwest::Client::builder()
        .danger_accept_invalid_certs(true)
        .build()?;
    
    client
        .post(url)
        .body(data.to_string())
        .send()
        .await?;
    
    Ok(())
}
use reqwest;
use std::sync::Arc;
use tokio;
use rustls::{Certificate, ClientConfig, RootCertStore};
use webpki_roots;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Secure: Use default certificate verification
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(10))
        .build()?;
    
    let response = client
        .get("https://api.example.com/data")
        .send()
        .await?;
    
    println!("Response: {}", response.text().await?);
    Ok(())
}

// Secure: Global client with proper TLS configuration
fn create_secure_client() -> Result<reqwest::Client, reqwest::Error> {
    reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .user_agent("SecureApp/1.0")
        // Certificate verification enabled by default
        .build()
}

lazy_static! {
    static ref HTTP_CLIENT: reqwest::Client = create_secure_client().unwrap();
}

pub async fn api_call(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    // Validate URL scheme
    let parsed_url = url::Url::parse(url)?;
    if parsed_url.scheme() != "https" {
        return Err("Only HTTPS URLs are allowed".into());
    }
    
    // Use secure global client
    let response = HTTP_CLIENT
        .get(url)
        .timeout(std::time::Duration::from_secs(10))
        .send()
        .await?;
    
    if !response.status().is_success() {
        return Err(format!("HTTP error: {}", response.status()).into());
    }
    
    Ok(response.text().await?)
}

pub async fn webhook_call(url: &str, data: &str) -> Result<(), Box<dyn std::error::Error>> {
    // Validate URL
    let parsed_url = url::Url::parse(url)?;
    if parsed_url.scheme() != "https" {
        return Err("Only HTTPS URLs are allowed for webhooks".into());
    }
    
    // Allowlist of permitted webhook hosts
    let allowed_hosts = vec![
        "webhook.trusted.com",
        "api.partner.com",
        "notify.secure.com",
    ];
    
    if let Some(host) = parsed_url.host_str() {
        if !allowed_hosts.contains(&host) {
            return Err("Webhook host not in allowlist".into());
        }
    } else {
        return Err("Invalid webhook URL".into());
    }
    
    // Secure webhook call
    let response = HTTP_CLIENT
        .post(url)
        .header("Content-Type", "application/json")
        .body(data.to_string())
        .timeout(std::time::Duration::from_secs(15))
        .send()
        .await?;
    
    if !response.status().is_success() {
        return Err(format!("Webhook failed: {}", response.status()).into());
    }
    
    Ok(())
}

// Advanced: Custom TLS configuration with certificate pinning
pub fn create_pinned_client(expected_cert_der: &[u8]) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
    let mut root_store = RootCertStore::empty();
    root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
        |ta| {
            rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
                ta.subject,
                ta.spki,
                ta.name_constraints,
            )
        },
    ));
    
    // Add pinned certificate
    let pinned_cert = Certificate(expected_cert_der.to_vec());
    
    let config = ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(root_store)
        .with_no_client_auth();
    
    let client = reqwest::Client::builder()
        .use_preconfigured_tls(config)
        .timeout(std::time::Duration::from_secs(30))
        .build()?;
    
    Ok(client)
}

// Secure configuration with custom CA
pub fn create_client_with_custom_ca(ca_cert_pem: &str) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
    let cert = reqwest::Certificate::from_pem(ca_cert_pem.as_bytes())?;
    
    let client = reqwest::Client::builder()
        .add_root_certificate(cert)
        .timeout(std::time::Duration::from_secs(30))
        .build()?;
    
    Ok(client)
}

// Error handling for certificate issues
pub async fn secure_api_call_with_retry(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let mut attempts = 0;
    let max_attempts = 3;
    
    while attempts < max_attempts {
        match api_call(url).await {
            Ok(response) => return Ok(response),
            Err(e) => {
                if e.to_string().contains("certificate") {
                    // Don't retry certificate errors
                    return Err(e);
                }
                
                attempts += 1;
                if attempts >= max_attempts {
                    return Err(e);
                }
                
                // Exponential backoff
                tokio::time::sleep(std::time::Duration::from_millis(100 * 2_u64.pow(attempts))).await;
            }
        }
    }
    
    Err("Max retries exceeded".into())
}

// Configuration struct for secure HTTP clients
#[derive(Debug, Clone)]
pub struct SecureHttpConfig {
    pub timeout: std::time::Duration,
    pub user_agent: String,
    pub allowed_hosts: Vec<String>,
    pub require_https: bool,
}

impl Default for SecureHttpConfig {
    fn default() -> Self {
        Self {
            timeout: std::time::Duration::from_secs(30),
            user_agent: "SecureApp/1.0".to_string(),
            allowed_hosts: vec![],
            require_https: true,
        }
    }
}

pub struct SecureHttpClient {
    client: reqwest::Client,
    config: SecureHttpConfig,
}

impl SecureHttpClient {
    pub fn new(config: SecureHttpConfig) -> Result<Self, reqwest::Error> {
        let client = reqwest::Client::builder()
            .timeout(config.timeout)
            .user_agent(&config.user_agent)
            .build()?;
        
        Ok(Self { client, config })
    }
    
    pub async fn get(&self, url: &str) -> Result<String, Box<dyn std::error::Error>> {
        self.validate_url(url)?;
        
        let response = self.client.get(url).send().await?;
        
        if !response.status().is_success() {
            return Err(format!("HTTP error: {}", response.status()).into());
        }
        
        Ok(response.text().await?)
    }
    
    fn validate_url(&self, url: &str) -> Result<(), Box<dyn std::error::Error>> {
        let parsed_url = url::Url::parse(url)?;
        
        if self.config.require_https && parsed_url.scheme() != "https" {
            return Err("Only HTTPS URLs are allowed".into());
        }
        
        if !self.config.allowed_hosts.is_empty() {
            if let Some(host) = parsed_url.host_str() {
                if !self.config.allowed_hosts.contains(&host.to_string()) {
                    return Err("Host not in allowlist".into());
                }
            }
        }
        
        Ok(())
    }
}

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Rust code disables cert validation: Client::builder().danger_accept_invalid_certs(true).build(). Accepts any TLS certificate including self-signed. Man-in-the-middle attacks possible. HTTPS security completely bypassed. danger_ prefix indicates security risk but sometimes used anyway.

Root causes

Using danger_accept_invalid_certs in reqwest Client

Rust code disables cert validation: Client::builder().danger_accept_invalid_certs(true).build(). Accepts any TLS certificate including self-signed. Man-in-the-middle attacks possible. HTTPS security completely bypassed. danger_ prefix indicates security risk but sometimes used anyway.

Development or Testing Code Left in Production

Debug code with disabled validation: if cfg!(debug_assertions) { builder.danger_accept_invalid_certs(true) }. Development patterns in production builds. Config flags failing. Test code paths active. Temporary debugging solutions becoming permanent.

Working Around Certificate Issues Instead of Fixing

Certificate errors bypassed: self-signed certificates, expired certs, hostname mismatches. danger_accept_invalid_certs quick fix. Root cause not addressed. Proper certificate management avoided. Security sacrificed for convenience.

Not Understanding TLS Certificate Validation Purpose

Developers unaware of validation importance. Believing HTTPS encryption alone sufficient. Not understanding authentication aspect. Missing man-in-the-middle attack knowledge. Encryption without authentication provides no security against active attackers.

Internal Services Assumed Safe Without Certificates

Internal API calls with disabled validation: assuming internal network secure. Lateral movement after initial compromise. Internal services should use TLS same as external. Zero-trust networking requires internal TLS validation.

Fixes

1

Never Use danger_accept_invalid_certs in Production

Remove all danger_accept_invalid_certs: Client::builder().build() defaults to strict validation. Search codebase: grep -r 'danger_accept_invalid_certs' --include="*.rs". Delete all instances. Code review rejecting this method. Certificate validation non-negotiable.

2

Add Custom CA Certificates for Self-Signed Certificates

Use reqwest Certificate: use reqwest::Certificate; let cert = Certificate::from_pem(&cert_bytes)?; Client::builder().add_root_certificate(cert).build()?. Add custom CA to trust store. Proper certificate validation with internal CAs. Maintains security while supporting self-signed.

3

Use Native TLS or Rustls with System Certificates

Default TLS backend uses system certificates: Client::builder().use_native_tls().build() or .use_rustls_tls().build(). System trust store automatically used. Updated with OS certificate updates. Proper CA chain validation. Modern TLS support.

4

Fix Certificate Errors Properly, Never Work Around

Address root causes: renew expired certificates; fix hostname mismatches; install proper CA certificates; use Let's Encrypt for free valid certs. Certificate errors indicate real problems. Security issues require fixing, not ignoring.

5

Use Certificate Pinning for Critical Services

Pin certificates: let cert = Certificate::from_pem(include_bytes!("server.crt"))?; Client::builder().add_root_certificate(cert).https_only(true).build()?. Specific certificate or CA. Prevents man-in-the-middle with compromised CAs. Critical for auth or payment services.

6

Enable HTTPS-Only Mode and Fail on Invalid Certificates

Strict HTTPS: Client::builder().https_only(true).build()?. Force HTTPS scheme. Fail on validation errors. No HTTP fallback. Explicit error handling. Log certificate failures for monitoring. Production should never accept invalid certificates.

Detect This Vulnerability in Your Code

Sourcery automatically identifies rust reqwest accept invalid certificates vulnerability and many other security issues in your codebase.