net/textproto: escape arbitrary input when including them in errors

When returning errors, functions in the net/textproto package would
include its input as part of the error, without any escaping. Note that
said input is often controlled by external parties when using this
package naturally. For example, a net/http client uses ReadMIMEHeader
when parsing the headers it receive from a server.

As a result, an attacker could inject arbitrary content into the error.
Practically, this can result in an attacker injecting misleading
content, terminal control bytes, etc. into a victim's output or logs.

Fix this issue by making sure that ProtocolError usages within the
package are properly escaped, and that Error.String will escape its Msg.

Fixes #79346
Fixes CVE-2026-42507

Change-Id: Ide4c1005d8254f90d95d7a389b8ca3a26a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/777060
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Nicholas S. Husin 2026-05-11 18:04:07 -04:00 committed by Nicholas Husin
parent ab7c8279a0
commit 1a7e601d07
4 changed files with 15 additions and 13 deletions

View file

@ -695,7 +695,7 @@ func TestHello(t *testing.T) {
err = c.Hello("customhost")
case 1:
err = c.StartTLS(nil)
if err.Error() == "502 Not implemented" {
if err.Error() == `502 "Not implemented"` {
err = nil
}
case 2:
@ -953,8 +953,8 @@ func TestAuthFailed(t *testing.T) {
if err == nil {
t.Error("Auth: expected error; got none")
} else if err.Error() != "535 Invalid credentials\nplease see www.example.com" {
t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com")
} else if err.Error() != `535 "Invalid credentials\nplease see www.example.com"` {
t.Errorf("Auth: got error: %v, want: %s", err, `535 "Invalid credentials\nplease see www.example.com"`)
}
bcmdbuf.Flush()

View file

@ -215,13 +215,13 @@ func (r *Reader) readCodeLine(expectCode int) (code int, continued bool, message
func parseCodeLine(line string, expectCode int) (code int, continued bool, message string, err error) {
if len(line) < 4 || line[3] != ' ' && line[3] != '-' {
err = ProtocolError("short response: " + line)
err = ProtocolError(fmt.Sprintf("short response: %q", line))
return
}
continued = line[3] == '-'
code, err = strconv.Atoi(line[0:3])
if err != nil || code < 100 {
err = ProtocolError("invalid response code: " + line)
err = ProtocolError(fmt.Sprintf("invalid response code: %q", line))
return
}
message = line[4:]
@ -253,7 +253,7 @@ func parseCodeLine(line string, expectCode int) (code int, continued bool, messa
func (r *Reader) ReadCodeLine(expectCode int) (code int, message string, err error) {
code, continued, message, err := r.readCodeLine(expectCode)
if err == nil && continued {
err = ProtocolError("unexpected multi-line response: " + message)
err = ProtocolError(fmt.Sprintf("unexpected multi-line response: %q", message))
}
return
}
@ -541,7 +541,7 @@ func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error)
if err != nil {
return m, err
}
return m, ProtocolError("malformed MIME header initial line: " + string(line))
return m, ProtocolError(fmt.Sprintf("malformed MIME header initial line: %q", line))
}
for {
@ -553,15 +553,15 @@ func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error)
// Key ends at first colon.
k, v, ok := bytes.Cut(kv, colon)
if !ok {
return m, ProtocolError("malformed MIME header line: " + string(kv))
return m, ProtocolError(fmt.Sprintf("malformed MIME header line: %q", kv))
}
key, ok := canonicalMIMEHeaderKey(k)
if !ok {
return m, ProtocolError("malformed MIME header line: " + string(kv))
return m, ProtocolError(fmt.Sprintf("malformed MIME header line: %q", kv))
}
for _, c := range v {
if !validHeaderValueByte(c) {
return m, ProtocolError("malformed MIME header line: " + string(kv))
return m, ProtocolError(fmt.Sprintf("malformed MIME header line: %q", kv))
}
}

View file

@ -411,6 +411,8 @@ func TestReadMultiLineError(t *testing.T) {
"Unexpected but legal text!\n" +
"5.1.1 https://support.google.com/mail/answer/6596 h20si25154304pfd.166 - gsmtp"
wantError := `550 "5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. Learn more at\nUnexpected but legal text!\n5.1.1 https://support.google.com/mail/answer/6596 h20si25154304pfd.166 - gsmtp"`
code, msg, err := r.ReadResponse(250)
if err == nil {
t.Errorf("ReadResponse: no error, want error")
@ -421,8 +423,8 @@ func TestReadMultiLineError(t *testing.T) {
if msg != wantMsg {
t.Errorf("ReadResponse: msg=%q, want %q", msg, wantMsg)
}
if err != nil && err.Error() != "550 "+wantMsg {
t.Errorf("ReadResponse: error=%q, want %q", err.Error(), "550 "+wantMsg)
if err != nil && err.Error() != wantError {
t.Errorf("ReadResponse: error=%q, want %q", err.Error(), wantError)
}
}

View file

@ -41,7 +41,7 @@ type Error struct {
}
func (e *Error) Error() string {
return fmt.Sprintf("%03d %s", e.Code, e.Msg)
return fmt.Sprintf("%03d %q", e.Code, e.Msg)
}
// A ProtocolError describes a protocol violation such