Repository: hetznercloud/hcloud-go
Pull request #161 (2021-01-08 11:22:39)

HTTP debug logs: Redact Authorization header
When using `hcloud.WithDebugWriter`, we currently output full HTTP requests and responses, including the `Authorization` header.

As we sometimes have to ask customers to provide debug logs, it's not ideal if they need to manually remove the header and may do so incompletely.

Redact the header in the output.



Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 4f7c9d6..b9dca13 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,9 @@
 # Changes
 
+## Unreleased
+
+* Fix Authorization header being included in debug output
+
 ## v1.23.1
 * Add removed `ErrorCodeServerAlreadyAttached` again
 
diff --git a/hcloud/client.go b/hcloud/client.go
index b03d45f..d79b0a8 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -204,8 +204,7 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) {
 		}
 
 		if c.debugWriter != nil {
-			// To get the response body we need to read it before the request was actually send. https://github.com/golang/go/issues/29792
-			dumpReq, err := httputil.DumpRequestOut(r, true)
+			dumpReq, err := dumpRequest(r)
 			if err != nil {
 				return nil, err
 			}
@@ -301,6 +300,25 @@ func (c *Client) buildUserAgent() {
 	}
 }
 
+func dumpRequest(r *http.Request) ([]byte, error) {
+	// Duplicate the request, so we can redact the auth header
+	rDuplicate := r.Clone(context.Background())
+	rDuplicate.Header.Set("Authorization", "REDACTED")
+
+	// To get the request body we need to read it before the request was actually sent.
+	// See https://github.com/golang/go/issues/29792
+	dumpReq, err := httputil.DumpRequestOut(rDuplicate, true)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set original request body to the duplicate created by DumpRequestOut. The request body is not duplicated
+	// by .Clone() and instead just referenced, so it would be completely read otherwise.
+	r.Body = rDuplicate.Body
+
+	return dumpReq, nil
+}
+
 func errorFromResponse(resp *http.Response, body []byte) error {
 	if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
 		return nil


Test patch:
diff --git a/hcloud/client_test.go b/hcloud/client_test.go
index 95cc796..6fb73d3 100644
--- a/hcloud/client_test.go
+++ b/hcloud/client_test.go
@@ -272,9 +272,16 @@ func TestClientDoPost(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
 
-	env.Client.debugWriter = new(bytes.Buffer)
+	debugLog := new(bytes.Buffer)
+
+	env.Client.debugWriter = debugLog
 	callCount := 0
 	env.Mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
+		auth := r.Header.Get("Authorization")
+		if auth != "Bearer token" {
+			t.Errorf("unexpected auth header: %q, expected %q", auth, "Bearer token")
+		}
+
 		callCount++
 		w.Header().Set("Content-Type", "application/json")
 		var dat map[string]interface{}
@@ -317,6 +324,10 @@ func TestClientDoPost(t *testing.T) {
 	if callCount != 2 {
 		t.Fatalf("unexpected callCount: %v", callCount)
 	}
+
+	if strings.Contains(debugLog.String(), "token") {
+		t.Errorf("debug log did contain token, although it shouldn't")
+	}
 }
 
 func TestBuildUserAgent(t *testing.T) {