Skip to content

[BUG] HTTP Header Injection (CRLF Injection) via Unsanitized Host Configuration in TCP Client #179

@EnthusiasticTech

Description

@EnthusiasticTech

Project

vgrep

Description

The HTTP client in src/server/client.rs constructs raw HTTP requests by directly interpolating user-configurable host and port values into the request headers without any CRLF (\r\n) sanitization. This allows an attacker who can control the server configuration (via config file, environment variables, or CLI arguments) to inject arbitrary HTTP headers into outgoing requests.

Vulnerable Code Locations

Line 73-84 (search method):

let request = format!(
    "POST /search HTTP/1.1\r\n\
     Host: {}\r\n\              // host_port directly interpolated
     Content-Type: application/json\r\n\
     Content-Length: {}\r\n\
     Connection: close\r\n\
     \r\n\
     {}",
    host_port,                   // <-- NO SANITIZATION
    body.len(),
    body
);

Line 121-132 (embed_batch method):

let http_request = format!(
    "POST /embed_batch HTTP/1.1\r\n\
     Host: {}\r\n\
     Content-Type: application/json\r\n\
     ...

Line 163-167 (health method):

let request = format!(
    "GET /health HTTP/1.1\r\n\
     Host: {}\r\n\
     ...

Attack Vector

The host value comes from:

  1. Config file (~/.vgrep/config.jsonserver_host)
  2. Environment variable (VGREP_HOST)
  3. CLI argument (--host)

If an attacker can set server_host to a value containing CRLF sequences, they can inject additional headers:

# Malicious host value
export VGREP_HOST=$'127.0.0.1\r\nX-Injected: malicious\r\nX-Another: header'
vgrep "test query"

This would produce:

POST /search HTTP/1.1
Host: 127.0.0.1
X-Injected: malicious
X-Another: header
Content-Type: application/json
...

Error Message

Debug Logs

System Information

Bounty Version: 0.1.0
OS: Ubuntu 24.04 LTS
CPU: AMD EPYC-Genoa Processor (8 cores)
RAM: 15 GB

Screenshots

No response

Steps to Reproduce

Method 1: Via Environment Variable

# Set malicious host with CRLF injection
export VGREP_HOST=$'127.0.0.1\r\nX-Pwned: true'

# Run any vgrep command that contacts the server
vgrep "test query"

# Or check the health endpoint
# The injected header will be sent in the HTTP request

Method 2: Via Config File

# Modify config to include CRLF in host
cat > ~/.vgrep/config.json << 'EOF'
{
  "server_host": "127.0.0.1\r\nX-Injected: malicious",
  "server_port": 7777
}
EOF

# Run vgrep - the injected headers are sent
vgrep status

Method 3: Test Script

#[test]
fn test_crlf_injection_vulnerability() {
    use vgrep::server::Client;
    
    // Malicious host with CRLF
    let malicious_host = "127.0.0.1\r\nX-Injected: evil";
    let client = Client::new(malicious_host, 7777);
    
    // The base_url now contains CRLF
    assert!(client.base_url.contains("\r\n"), "CRLF should be blocked!");
}

Expected Behavior

  • The client should reject or sanitize host values containing CRLF characters (\r, \n)
  • Invalid characters in the host should cause an error, not be silently accepted
  • HTTP headers should be properly escaped or constructed using a safe HTTP library

Actual Behavior

  • CRLF characters in the host configuration are passed through without validation
  • The raw HTTP request includes the injected headers
  • No error or warning is shown to the user
  • Attacker can inject arbitrary HTTP headers into all outgoing requests

Additional Context

Security Impact

Aspect Impact
Severity HIGH
CVSS Score 7.5 (High)
Attack Vector Local (requires config/env access)
Privileges Required Low (can write to config or set env)
Impact Request smuggling, cache poisoning, auth bypass

Potential Attack Scenarios

  1. Request Smuggling: Inject Transfer-Encoding: chunked or Content-Length to cause request smuggling
  2. Cache Poisoning: Inject X-Forwarded-Host to poison caches
  3. Authentication Bypass: Inject Authorization headers if vgrep server is behind a proxy
  4. Session Hijacking: Inject Cookie headers if targeting a web service

Why This Is Different From Existing Issues

Existing Issue This Bug
#115 - Response size limit This is about request injection, not response handling
#98 - No timeout This is about header injection, not timing
#39 - Chunked encoding This is about injecting headers, not parsing responses
#124 - CORS This is client-side injection, not server-side CORS

Suggested Fix

Option 1: Validate host at construction time

impl Client {
    pub fn new(host: &str, port: u16) -> Result<Self, Error> {
        // Reject CRLF in host
        if host.contains('\r') || host.contains('\n') {
            return Err(Error::InvalidHost("Host contains invalid characters"));
        }
        Ok(Self {
            base_url: format!("http://{}:{}", host, port),
        })
    }
}

Option 2: Use a proper HTTP library

// Instead of raw TCP, use reqwest or ureq
use reqwest::blocking::Client;

impl VgrepClient {
    pub fn search(&self, query: &str) -> Result<SearchResponse> {
        let client = Client::new();
        let response = client
            .post(&format!("{}/search", self.base_url))
            .json(&SearchRequest { query: query.to_string(), ..Default::default() })
            .send()?;
        Ok(response.json()?)
    }
}

Option 3: Escape special characters

fn sanitize_host(host: &str) -> String {
    host.chars()
        .filter(|c| !matches!(c, '\r' | '\n' | '\0'))
        .collect()
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingideIssues related to IDEinvalidThis doesn't seem rightvgrep

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions