VYPR
Medium severity5.3NVD Advisory· Published Apr 1, 2025· Updated Apr 15, 2026

CVE-2025-31135

CVE-2025-31135

Description

Go-Guerrilla SMTP Daemon is a lightweight SMTP server written in Go. Prior to 1.6.7, when ProxyOn is enabled, the PROXY command will be accepted multiple times, with later invocations overriding earlier ones. The proxy protocol only supports one initial PROXY header; anything after that is considered part of the exchange between client and server, so the client is free to send further PROXY commands with whatever data it pleases. go-guerrilla will treat these as coming from the reverse proxy, allowing a client to spoof its IP address. This vulnerability is fixed in 1.6.7.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

In Go-Guerrilla SMTP Daemon before 1.6.7, the PROXY command can be sent multiple times, allowing a client to spoof its IP address when proxy protocol is enabled.

Vulnerability

Analysis

Go-Guerrilla SMTP Daemon, a lightweight SMTP server written in Go, is vulnerable to IP address spoofing when proxy protocol support (ProxyOn) is enabled. The root cause is that the server accepts the PROXY command multiple times, with later invocations overriding earlier ones. According to the official description [2] and the security advisory [4], the proxy protocol only supports a single initial PROXY header; any subsequent PROXY commands should be treated as part of the client-server exchange. This design flaw allows an attacker to inject crafted PROXY headers after the legitimate one, effectively spoofing the source IP address seen by the server.

Exploitation

The attacker must be able to establish a TCP connection to the Go-Guerrilla SMTP instance with ProxyOn enabled [1]. No authentication is required before sending the PROXY command, as the proxy header is expected before any SMTP conversation. The exploit is straightforward: after the reverse proxy sends its initial valid PROXY header, the attacker (the client behind the proxy) can send additional PROXY commands with arbitrary IP addresses. Go-Guerrilla will accept these subsequent commands and override the previously stored remote IP, treating it as if it came from the reverse proxy [4]. The commit fix [3] shows that the vulnerability is addressed by properly limiting PROXY command acceptance and improving header detection.

Impact

An attacker can spoof the RemoteIP field within the Go-Guerrilla SMTP daemon. While the advisory [4] notes that the practical impact on a Mail Transfer Agent (MTA) is limited compared to web servers, spoofed IP addresses can bypass IP-based access controls, logging, reputation systems, or anti-spam measures that rely on the client IP. Attackers could send emails that appear to originate from trusted or whitelisted IP addresses, potentially evading filtering or attribution.

Mitigation

The vulnerability is fixed in version 1.6.7 [2][4]. Users should update immediately. The fix involves changes in command parsing to prevent re-processing of the PROXY header after the initial valid one [3]. There is no indication of a workaround beyond disabling the proxy protocol (ProxyOn: false) if upgrading is not immediately possible. No known public exploit code is available at this time, but the advisory confirms the attack vector is trivial to execute.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/phires/go-guerrillaGo
< 1.6.71.6.7

Patches

2
d08fe22f8022

Prevent IP address spoofing with PROXY command

https://github.com/phires/go-guerrillaPaul BuonopaneMar 26, 2025via ghsa
2 files changed · +117 17
  • client.go+6 2 modified
    @@ -21,8 +21,12 @@ import (
     type ClientState int
     
     const (
    -	// The client has connected, and is awaiting our first response
    -	ClientGreeting = iota
    +	// The client has connected
    +	ClientConnected = iota
    +	// We're awaiting a PROXY header from the client
    +	ClientProxy
    +	// The client is awaiting our first response
    +	ClientGreeting
     	// We have responded to the client's connection and are awaiting a command
     	ClientCmd
     	// We have received the sender and recipient information
    
  • server.go+111 15 modified
    @@ -8,6 +8,7 @@ import (
     	"fmt"
     	"io"
     	"net"
    +	"net/netip"
     	"os"
     	"path/filepath"
     	"strings"
    @@ -82,7 +83,13 @@ var (
     	cmdQUIT     command = []byte("QUIT")
     	cmdDATA     command = []byte("DATA")
     	cmdSTARTTLS command = []byte("STARTTLS")
    -	cmdPROXY    command = []byte("PROXY")
    +	// PROXY isn't part of the SMTP protocol; instead, it encapsulates the SMTP conversation.
    +	// The conversation is prefixed with a header that always starts with []byte("PROXY "),
    +	// so we can reuse the command logic.
    +	cmdPROXY command = []byte("PROXY ")
    +	// PROXY v2 binary format header. Not currently supported, but we can at least
    +	// detect it and emit a helpful error.
    +	cmdPROXY2 command = []byte("\x0D\x0A\x0D\x0A\x00\x0D\x0AQUIT\x0A")
     )
     
     func (c command) match(in []byte) bool {
    @@ -338,12 +345,18 @@ const commandSuffix = "\r\n"
     
     // Reads from the client until a \n terminator is encountered,
     // or until a timeout occurs.
    +//
    +// Removes the trailing \n and and any preceding \r.
    +//
    +// Note that the returned slice is only valid until the next
    +// read. Callers are responsible for copying the data if it must
    +// persist between reads.
     func (s *server) readCommand(client *client) ([]byte, error) {
     	//var input string
     	var err error
     	var bs []byte
     	// In command state, stop reading at line breaks
    -	bs, err = client.bufin.ReadSlice('\n')
    +	bs, err = s.readSlice(client, '\n')
     	if err != nil {
     		return bs, err
     	} else if bytes.HasSuffix(bs, []byte(commandSuffix)) {
    @@ -352,6 +365,16 @@ func (s *server) readCommand(client *client) ([]byte, error) {
     	return bs[:len(bs)-1], err
     }
     
    +// Reads from the client until delim is encountered. The delimiter
    +// is preserved.
    +//
    +// Note that the returned slice is only valid until the next
    +// read. Callers are responsible for copying the data if it must
    +// persist between reads.
    +func (s *server) readSlice(client *client, delim byte) ([]byte, error) {
    +	return client.bufin.ReadSlice(delim)
    +}
    +
     // flushResponse a response to the client. Flushes the client.bufout buffer to the connection
     func (s *server) flushResponse(client *client) error {
     	if err := client.setTimeout(s.timeout.Load().(time.Duration)); err != nil {
    @@ -407,9 +430,95 @@ func (s *server) handleClient(client *client) {
     	r := response.Canned
     	for client.isAlive() {
     		switch client.state {
    +		case ClientConnected:
    +			if sc.ProxyOn {
    +				client.state = ClientProxy
    +			} else {
    +				client.state = ClientGreeting
    +			}
    +
    +		case ClientProxy:
    +			// Worst case v1 header length is 107 bytes per https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt#:~:text=The%20maximum%20line%20lengths%20the%20receiver%20must%20support%20including%20the%20CRLF%20are
    +			client.bufin.setLimit(107)
    +			input, err := s.readSlice(client, '\n')
    +			if err != nil {
    +				// We don't care about the specific error; regardless of the error type, we need to kill the connection
    +				s.log().WithError(err).Warnf("Error reading PROXY header from %s. Disable \"proxyon\" in your configuration file if you're not using a reverse proxy.", client.RemoteIP)
    +				// Depending on the error, the client might already be dead.
    +				if client.isAlive() {
    +					client.kill()
    +				}
    +				return
    +			}
    +
    +			// PROXY is its own protocol; it's not part of SMTP, and it has different constraints.
    +			// Notably:
    +			//  - The prefix must be exactly []byte("PROXY ") (note the space)
    +			//  - All parameters must be separated by exactly one space
    +			//  - The header must be terminated by []byte("\r\n"), not just '\n'
    +			if cmdPROXY.match(input) && bytes.HasSuffix(input, []byte("\r\n")) {
    +				// Any data we use from params/input must be copied if it persists beyond this loop.
    +				input = input[0 : len(input)-2] // Remove trailing "\r\n"
    +				s.log().Debugf("Received PROXY header: %s", input)
    +
    +				params := bytes.Split(input, []byte{' '})
    +				params = params[1:] // Remove "PROXY" prefix
    +				if len(params) < 1 {
    +					s.log().Fatal("logic error: because we check for \"PROXY \", including the trailing space, we should see at least one param")
    +					client.kill()
    +					return
    +				}
    +
    +				proto := string(params[0])
    +				switch proto {
    +				case "UNKNOWN":
    +					// Remaining params are optional.
    +					client.RemoteIP = ""
    +					s.log().Infof("Proxying from %s", proto)
    +				case "TCP4", "TCP6":
    +					ipStr := string(params[1])
    +					if ip, err := netip.ParseAddr(ipStr); err != nil {
    +						s.log().WithError(err).Errorf("Invalid IP address in PROXY header from %s: %s", client.RemoteIP, string(params[1]))
    +						client.kill()
    +						return
    +					} else if ipStr != ip.String() {
    +						s.log().Errorf("Source IP address in PROXY header must be normalized per proxy protocol spec, but the IP address sent by the proxy isn't normalized. Received %s, but the normalized form is %s.", ipStr, ip.String())
    +						client.kill()
    +						return
    +					} else if ip.Is4() != (proto == "TCP4") {
    +						var ipVersion string
    +						if ip.Is4() {
    +							ipVersion = "IPv4"
    +						} else {
    +							ipVersion = "IPv6"
    +						}
    +						s.log().Errorf("PROXY header specified protocol %s, but offered an %s address: %v", proto, ipVersion, ip)
    +					}
    +					// TODO We may want to validate the destination IP address, source port, and destination port as well.
    +					client.RemoteIP = ipStr
    +					s.log().Infof("Proxying from %s %v", proto, ipStr)
    +				default:
    +					s.log().Errorf("PROXY header specified unknown protocol %s", proto)
    +					client.kill()
    +					return
    +				}
    +				client.state = ClientGreeting
    +			} else if cmdPROXY2.match(input) {
    +				// Note: If we ever decide to support v2, we need to re-evaluate the previous call to
    +				// client.bufin.setLimit. The limit we currently set--107 bytes--is for v1.
    +				s.log().Warnf("Received a connection with a PROXY v2 binary header, but we only support v1: %s", client.RemoteIP)
    +				client.kill()
    +				return
    +			} else {
    +				s.log().Warnf("Initial command wasn't a valid PROXY header from %s. Disable \"proxyon\" in your configuration file if you're not using a reverse proxy.", client.RemoteIP)
    +				client.kill()
    +				return
    +			}
    +
     		case ClientGreeting:
     			client.sendResponse(greeting)
     			client.state = ClientCmd
    +
     		case ClientCmd:
     			client.bufin.setLimit(CommandLineMaxLength)
     			input, err := s.readCommand(client)
    @@ -492,19 +601,6 @@ func (s *server) handleClient(client *client) {
     				}
     				client.sendResponse(r.SuccessMailCmd)
     
    -			case sc.ProxyOn && cmdPROXY.match(cmd):
    -				if toks := bytes.Split(input[6:], []byte{' '}); len(toks) == 5 {
    -					s.log().Debugf("PROXY command. Proto: [%s] Source IP: [%s] Dest IP: [%s] Source Port: [%s] Dest Port: [%s]", toks[0], toks[1], toks[2], toks[3], toks[4])
    -					client.RemoteIP = string(toks[1])
    -					s.log().Debugf("client.RemoteIP: [%s]", client.RemoteIP)
    -					// There is RfC or anything about the PROXY command,
    -					// so it is unclear, if a response is required.
    -					//client.sendResponse(r.SuccessMailCmd)
    -				} else {
    -					s.log().Error("PROXY parse error", "["+string(input[6:])+"]")
    -					client.sendResponse(r.FailSyntaxError)
    -				}
    -
     			case cmdMAIL.match(cmd):
     				if client.isInTransaction() {
     					client.sendResponse(r.FailNestedMailCmd)
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.