Skip to content

Commit

Permalink
Adding support for securing error logs (#1001)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Firsht <[email protected]>
  • Loading branch information
dfirsht and Daniel Firsht authored Mar 26, 2021
1 parent e61c9d5 commit 2a6f7db
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 7 deletions.
14 changes: 14 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,14 @@ type HostClient struct {
// extra slashes are removed, special characters are encoded.
DisablePathNormalizing bool

// Will not log potentially sensitive content in error logs
//
// This option is useful for servers that handle sensitive data
// in the request/response.
//
// Client logs full errors by default.
SecureErrorLogMessage bool

// Maximum duration for waiting for a free connection.
//
// By default will not waiting, return ErrNoFreeConns immediately
Expand Down Expand Up @@ -1332,6 +1340,12 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error)
panic("BUG: resp cannot be nil")
}

// Secure header error logs configuration
resp.secureErrorLogMessage = c.SecureErrorLogMessage
resp.Header.secureErrorLogMessage = c.SecureErrorLogMessage
req.secureErrorLogMessage = c.SecureErrorLogMessage
req.Header.secureErrorLogMessage = c.SecureErrorLogMessage

if c.IsTLS != bytes.Equal(req.uri.Scheme(), strHTTPS) {
return false, ErrHostClientRedirectToDifferentScheme
}
Expand Down
39 changes: 32 additions & 7 deletions header.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type ResponseHeader struct {
statusCode int
contentLength int
contentLengthBytes []byte
secureErrorLogMessage bool

contentType []byte
server []byte
Expand Down Expand Up @@ -65,6 +66,7 @@ type RequestHeader struct {

contentLength int
contentLengthBytes []byte
secureErrorLogMessage bool

method []byte
requestURI []byte
Expand Down Expand Up @@ -1354,6 +1356,11 @@ func (h *ResponseHeader) tryRead(r *bufio.Reader, n int) error {

// This is for go 1.6 bug. See https://github.com/golang/go/issues/14121 .
if err == bufio.ErrBufferFull {
if h.secureErrorLogMessage {
return &ErrSmallBuffer{
error: fmt.Errorf("error when reading response headers"),
}
}
return &ErrSmallBuffer{
error: fmt.Errorf("error when reading response headers: %s", errSmallBuffer),
}
Expand All @@ -1364,15 +1371,15 @@ func (h *ResponseHeader) tryRead(r *bufio.Reader, n int) error {
b = mustPeekBuffered(r)
headersLen, errParse := h.parse(b)
if errParse != nil {
return headerError("response", err, errParse, b)
return headerError("response", err, errParse, b, h.secureErrorLogMessage)
}
mustDiscard(r, headersLen)
return nil
}

func headerError(typ string, err, errParse error, b []byte) error {
func headerError(typ string, err, errParse error, b []byte, secureErrorLogMessage bool) error {
if errParse != errNeedMore {
return headerErrorMsg(typ, errParse, b)
return headerErrorMsg(typ, errParse, b, secureErrorLogMessage)
}
if err == nil {
return errNeedMore
Expand All @@ -1385,14 +1392,17 @@ func headerError(typ string, err, errParse error, b []byte) error {
}

if err != bufio.ErrBufferFull {
return headerErrorMsg(typ, err, b)
return headerErrorMsg(typ, err, b, secureErrorLogMessage)
}
return &ErrSmallBuffer{
error: headerErrorMsg(typ, errSmallBuffer, b),
error: headerErrorMsg(typ, errSmallBuffer, b, secureErrorLogMessage),
}
}

func headerErrorMsg(typ string, err error, b []byte) error {
func headerErrorMsg(typ string, err error, b []byte, secureErrorLogMessage bool) error {
if secureErrorLogMessage {
return fmt.Errorf("error when reading %s headers: %s. Buffer size=%d", typ, err, len(b))
}
return fmt.Errorf("error when reading %s headers: %s. Buffer size=%d, contents: %s", typ, err, len(b), bufferSnippet(b))
}

Expand Down Expand Up @@ -1444,7 +1454,7 @@ func (h *RequestHeader) tryRead(r *bufio.Reader, n int) error {
b = mustPeekBuffered(r)
headersLen, errParse := h.parse(b)
if errParse != nil {
return headerError("request", err, errParse, b)
return headerError("request", err, errParse, b, h.secureErrorLogMessage)
}
mustDiscard(r, headersLen)
return nil
Expand Down Expand Up @@ -1724,6 +1734,9 @@ func (h *ResponseHeader) parseFirstLine(buf []byte) (int, error) {
// parse protocol
n := bytes.IndexByte(b, ' ')
if n < 0 {
if h.secureErrorLogMessage {
return 0, fmt.Errorf("cannot find whitespace in the first line of response")
}
return 0, fmt.Errorf("cannot find whitespace in the first line of response %q", buf)
}
h.noHTTP11 = !bytes.Equal(b[:n], strHTTP11)
Expand All @@ -1732,9 +1745,15 @@ func (h *ResponseHeader) parseFirstLine(buf []byte) (int, error) {
// parse status code
h.statusCode, n, err = parseUintBuf(b)
if err != nil {
if h.secureErrorLogMessage {
return 0, fmt.Errorf("cannot parse response status code: %s", err)
}
return 0, fmt.Errorf("cannot parse response status code: %s. Response %q", err, buf)
}
if len(b) > n && b[n] != ' ' {
if h.secureErrorLogMessage {
return 0, fmt.Errorf("unexpected char at the end of status code")
}
return 0, fmt.Errorf("unexpected char at the end of status code. Response %q", buf)
}

Expand All @@ -1754,6 +1773,9 @@ func (h *RequestHeader) parseFirstLine(buf []byte) (int, error) {
// parse method
n := bytes.IndexByte(b, ' ')
if n <= 0 {
if h.secureErrorLogMessage {
return 0, fmt.Errorf("cannot find http request method")
}
return 0, fmt.Errorf("cannot find http request method in %q", buf)
}
h.method = append(h.method[:0], b[:n]...)
Expand All @@ -1767,6 +1789,9 @@ func (h *RequestHeader) parseFirstLine(buf []byte) (int, error) {
n = len(b)
protoStr = strHTTP10
} else if n == 0 {
if h.secureErrorLogMessage {
return 0, fmt.Errorf("requestURI cannot be empty")
}
return 0, fmt.Errorf("requestURI cannot be empty in %q", buf)
} else if !bytes.Equal(b[n+1:], strHTTP11) {
h.noHTTP11 = true
Expand Down
73 changes: 73 additions & 0 deletions header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2399,6 +2399,31 @@ func TestResponseHeaderReadError(t *testing.T) {
testResponseHeaderReadError(t, h, "HTTP/1.1 200 OK\r\nContent-Length: 123\r\nContent-Type: text/html\r\n")
}

func TestResponseHeaderReadErrorSecureLog(t *testing.T) {
h := &ResponseHeader{
secureErrorLogMessage: true,
}

// incorrect first line
testResponseHeaderReadSecuredError(t, h, "fo")
testResponseHeaderReadSecuredError(t, h, "foobarbaz")
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1")
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1 ")
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1 s")

// non-numeric status code
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1 foobar OK\r\nContent-Length: 123\r\nContent-Type: text/html\r\n\r\n")
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1 123foobar OK\r\nContent-Length: 123\r\nContent-Type: text/html\r\n\r\n")
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1 foobar344 OK\r\nContent-Length: 123\r\nContent-Type: text/html\r\n\r\n")


// no headers
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1 200 OK\r\n")

// no trailing crlf
testResponseHeaderReadSecuredError(t, h, "HTTP/1.1 200 OK\r\nContent-Length: 123\r\nContent-Type: text/html\r\n")
}

func TestRequestHeaderReadError(t *testing.T) {
t.Parallel()

Expand All @@ -2417,14 +2442,47 @@ func TestRequestHeaderReadError(t *testing.T) {
testRequestHeaderReadError(t, h, "POST /a HTTP/1.1\r\nHost: bb\r\nContent-Type: aa\r\nContent-Length: dff\r\n\r\nqwerty")
}

func TestRequestHeaderReadSecuredError(t *testing.T) {
t.Parallel()

h := &RequestHeader{
secureErrorLogMessage: true,
}

// incorrect first line
testRequestHeaderReadSecuredError(t, h, "fo")
testRequestHeaderReadSecuredError(t, h, "GET ")
testRequestHeaderReadSecuredError(t, h, "GET / HTTP/1.1\r")

// missing RequestURI
testRequestHeaderReadSecuredError(t, h, "GET HTTP/1.1\r\nHost: google.com\r\n\r\n")

// post with invalid content-length
testRequestHeaderReadSecuredError(t, h, "POST /a HTTP/1.1\r\nHost: bb\r\nContent-Type: aa\r\nContent-Length: dff\r\n\r\nqwerty")
}

func testResponseHeaderReadError(t *testing.T, h *ResponseHeader, headers string) {
r := bytes.NewBufferString(headers)
br := bufio.NewReader(r)
err := h.Read(br)
if err == nil {
t.Fatalf("Expecting error when reading response header %q", headers)
}
// make sure response header works after error
testResponseHeaderReadSuccess(t, h, "HTTP/1.1 200 OK\r\nContent-Type: foo/bar\r\nContent-Length: 12345\r\n\r\nsss",
200, 12345, "foo/bar", "sss")
}

func testResponseHeaderReadSecuredError(t *testing.T, h *ResponseHeader, headers string) {
r := bytes.NewBufferString(headers)
br := bufio.NewReader(r)
err := h.Read(br)
if err == nil {
t.Fatalf("Expecting error when reading response header %q", headers)
}
if strings.Contains(err.Error(), headers) {
t.Fatalf("Not expecting header content in err %q", err)
}
// make sure response header works after error
testResponseHeaderReadSuccess(t, h, "HTTP/1.1 200 OK\r\nContent-Type: foo/bar\r\nContent-Length: 12345\r\n\r\nsss",
200, 12345, "foo/bar", "sss")
Expand All @@ -2443,6 +2501,21 @@ func testRequestHeaderReadError(t *testing.T, h *RequestHeader, headers string)
-2, "/foo/bar", "aaaa", "", "", "xxx")
}

func testRequestHeaderReadSecuredError(t *testing.T, h *RequestHeader, headers string) {
r := bytes.NewBufferString(headers)
br := bufio.NewReader(r)
err := h.Read(br)
if err == nil {
t.Fatalf("Expecting error when reading request header %q", headers)
}
if strings.Contains(err.Error(), headers) {
t.Fatalf("Not expecting header content in err %q", err)
}
// make sure request header works after error
testRequestHeaderReadSuccess(t, h, "GET /foo/bar HTTP/1.1\r\nHost: aaaa\r\n\r\nxxx",
-2, "/foo/bar", "aaaa", "", "", "xxx")
}

func testResponseHeaderReadSuccess(t *testing.T, h *ResponseHeader, headers string, expectedStatusCode, expectedContentLength int,
expectedContentType, expectedTrailer string) {
r := bytes.NewBufferString(headers)
Expand Down
5 changes: 5 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Request struct {

multipartForm *multipart.Form
multipartFormBoundary string
secureErrorLogMessage bool

// Group bool members in order to reduce Request object size.
parsedURI bool
Expand Down Expand Up @@ -88,6 +89,7 @@ type Response struct {
SkipBody bool

keepBodyBuffer bool
secureErrorLogMessage bool

// Remote TCPAddr from concurrently net.Conn
raddr net.Addr
Expand Down Expand Up @@ -1368,6 +1370,9 @@ func (req *Request) Write(w *bufio.Writer) error {
if hasBody {
_, err = w.Write(body)
} else if len(body) > 0 {
if req.secureErrorLogMessage {
return fmt.Errorf("non-zero body for non-POST request")
}
return fmt.Errorf("non-zero body for non-POST request. body=%q", body)
}
return err
Expand Down
14 changes: 14 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,14 @@ type Server struct {
// are suppressed in order to limit output log traffic.
LogAllErrors bool

// Will not log potentially sensitive content in error logs
//
// This option is useful for servers that handle sensitive data
// in the request/response.
//
// Server logs all full errors by default.
SecureErrorLogMessage bool

// Header names are passed as-is without normalization
// if this option is set.
//
Expand Down Expand Up @@ -2055,6 +2063,12 @@ func (s *Server) serveConn(c net.Conn) (err error) {
ctx.Response.Header.noDefaultContentType = s.NoDefaultContentType
ctx.Response.Header.noDefaultDate = s.NoDefaultDate

// Secure header error logs configuration
ctx.Request.Header.secureErrorLogMessage = s.SecureErrorLogMessage
ctx.Response.Header.secureErrorLogMessage = s.SecureErrorLogMessage
ctx.Request.secureErrorLogMessage = s.SecureErrorLogMessage
ctx.Response.secureErrorLogMessage = s.SecureErrorLogMessage

if err == nil {
if s.ReadTimeout > 0 {
if err := c.SetReadDeadline(time.Now().Add(s.ReadTimeout)); err != nil {
Expand Down

0 comments on commit 2a6f7db

Please sign in to comment.