Skip to content

Potential security issue (X-Real-IP spoofing) #634

@mandreko

Description

@mandreko

I've been reviewing the RustDesk server source code and noticed that the WebSocket implementation seemingly trusts the X-Real-IP and X-Forwarded-For HTTP headers without validation. I'm trying to determine if this is an intentional design decision or a potential security vulnerability. I'm guessing it's to do with handling if the RustDesk server is behind a reverse-proxy or not, but I don't see any validation performed in that case. This would be similar to CVE-2025-6504, CVE-2025-69203, CVE-2024-0970, and many others.

I'd appreciate guidance on whether this constitutes a valid security finding since I lack some of the context of the development team, and this could totally be a nothing-burger.

The code in question

The source in question:
https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L1160-L1177
https://github.com/rustdesk/rustdesk-server/blob/master/src/relay_server.rs#L400-L416

When handling WebSocket connections, the server extracts the client IP from HTTP headers. The server then uses this addr throughout the codebase for various operations. I don't see any validation to check if the header value matches the actual TCP peer address.

Where does this spoofed addr get used?

I traced it through the codebase to see what actually relies on it:

Rate limiting - When processing RegisterPk messages https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L350-L352

The check_ip_blocker function (lines 891-919) maintains per-IP counters for rate limiting. When processing RegisterPk messages, this uses the IP from addr.

Potential Impact: An attacker could bypass rate limiting by changing the X-Real-IP header on each request.

Limitation: Only applies to RegisterPk messages, not basic WebSocket connections.

2. Internal Tracking Structures

Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L719-L733 (Punch_REQ)
Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L892-L917 (IP_BLOCKER)
Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L1017-L1048 (IP_CHANGES)

These data structures store the IP for monitoring and security purposes.

Potential Impact: An attacker could poison these internal records with false IPs, affecting monitoring and security decisions.

Limitation: These structures are only viewable via localhost admin interface or management command port (not sure what this is called).

3. Authentication Failure Logging

Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L691

Potential Impact: Logs would show the spoofed IP rather than the real attacker's IP, potentially causing false attribution.

Limitation: Only logs on authentication failures; most operations don't log IPs at all. (Pro-only feature, since OSS doesn't use auth as far as I can tell?)

4. IP Change Detection

Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L605

Potential Impact: IP change detection could be triggered or evaded using spoofed headers.

Limitation: Only triggers when a peer's IP actually changes.

5. Blocklist/Blacklist Checks

Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/relay_server.rs#L382-L385

Potential Impact: A blocked IP could potentially evade the block by spoofing a different IP.

Limitation: Unclear how effective this would be in practice.

What Does NOT Use X-Real-IP?

Through testing and code review, I found that some security features correctly use the actual TCP peer address:

1. Admin Commands (Secure)

Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L1104-L1117

This checks the actual TCP connection source, not WebSocket headers. Admin commands are not vulnerable to X-Real-IP spoofing.

2. ConfigUpdate Messages (Secure)

Location: https://github.com/rustdesk/rustdesk-server/blob/master/src/rendezvous_server.rs#L445-L465

While this checks is_loopback(), the ConfigureUpdate message is only processed via UDP (in handle_udp), not via WebSocket. The addr here comes from the UDP packet source, not HTTP headers. This is not vulnerable to X-Real-IP spoofing.

Questions for Maintainers

  1. Is X-Real-IP trust intentional?
    Is the server designed to be deployed behind a reverse proxy that validates these headers? If so, should this be documented as a deployment requirement? Or should this functionality have an opt-in switch to allow users to specify if these headers should be trusted in the case of a reverse proxy?

  2. What is the intended deployment model?

    • Direct internet exposure?
    • Behind nginx/reverse proxy?
    • Behind cloud load balancer?
  3. Are the security implications understood and correct?
    Specifically:

    • Rate limiting bypass via RegisterPk messages
    • IP-based access control evasion
    • Audit log integrity concerns

Proposed Fix (If Considered a Vulnerability)

Option 1: Remove X-Real-IP Trust (Most Secure)

// REMOVE the callback that reads headers
let ws_stream = tokio_tungstenite::accept_async(stream).await?;
// Use actual TCP peer address
let addr = stream.peer_addr()?;

This ensures IP-based security decisions use the actual source.

Option 2: Add Configuration Option

// Only trust X-Real-IP if explicitly enabled
if config.trust_proxy_headers {
    // Read X-Real-IP
} else {
    // Use peer_addr()
}

With clear documentation about the security implications.

Option 3: Validate Header Against Peer Address

let real_ip = headers.get("X-Real-IP");
let peer_ip = stream.peer_addr()?.ip();
if let Some(ip) = real_ip {
    // Only accept if from trusted proxy IP
    if is_trusted_proxy(peer_ip) {
        addr = format!("{ip}:0").parse().unwrap_or(addr);
    }
}

Option 4: Document as Deployment Requirement

If the current behavior is intentional, clearly document:

  • Server MUST be deployed behind reverse proxy
  • Proxy MUST strip/validate X-Real-IP headers
  • Direct internet exposure is NOT supported

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions