Skip to content

Commit 76d9391

Browse files
TPT-4014: Redact sensitive data from logging (#906)
* Redact sensitive data from logging * Address copilot suggestions * TPT-4014 Address comments suggestions
1 parent cf20b58 commit 76d9391

2 files changed

Lines changed: 134 additions & 4 deletions

File tree

client.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ Body: {{.Body}}`))
6666

6767
var envDebug = false
6868

69+
// redactHeadersMap is a map of headers that should be redacted in logs,
70+
// mapping the header name to its redacted value.
71+
var redactHeadersMap = map[string]string{
72+
"Authorization": "Bearer *******************************",
73+
}
74+
6975
// Client is a wrapper around the Resty client
7076
type Client struct {
7177
resty *resty.Client
@@ -394,6 +400,19 @@ func (c *httpClient) applyAfterResponse(resp *http.Response) error {
394400
return nil
395401
}
396402

403+
// nolint:unused
404+
func redactHeaders(headers http.Header) http.Header {
405+
redacted := headers.Clone()
406+
407+
for header, redactedValue := range redactHeadersMap {
408+
if headers.Get(header) != "" {
409+
redacted.Set(header, redactedValue)
410+
}
411+
}
412+
413+
return redacted
414+
}
415+
397416
// nolint:unused
398417
func (c *httpClient) logRequest(req *http.Request, method, url string, bodyBuffer *bytes.Buffer) {
399418
var reqBody string
@@ -408,7 +427,7 @@ func (c *httpClient) logRequest(req *http.Request, method, url string, bodyBuffe
408427
err := reqLogTemplate.Execute(&logBuf, map[string]any{
409428
"Method": method,
410429
"URL": url,
411-
"Headers": req.Header,
430+
"Headers": redactHeaders(req.Header),
412431
"Body": reqBody,
413432
})
414433
if err == nil {
@@ -456,7 +475,7 @@ func (c *httpClient) logResponse(resp *http.Response) (*http.Response, error) {
456475

457476
err := respLogTemplate.Execute(&logBuf, map[string]any{
458477
"Status": resp.Status,
459-
"Headers": resp.Header,
478+
"Headers": redactHeaders(resp.Header),
460479
"Body": respBody.String(),
461480
})
462481
if err == nil {
@@ -827,10 +846,22 @@ func (c *Client) updateHostURL() {
827846
)
828847
}
829848

849+
func redactLogHeaders(header http.Header) {
850+
for h, redactedValue := range redactHeadersMap {
851+
if header.Get(h) != "" {
852+
header.Set(h, redactedValue)
853+
}
854+
}
855+
}
856+
830857
func (c *Client) enableLogSanitization() *Client {
831858
c.resty.OnRequestLog(func(r *resty.RequestLog) error {
832-
// masking authorization header
833-
r.Header.Set("Authorization", "Bearer *******************************")
859+
redactLogHeaders(r.Header)
860+
return nil
861+
})
862+
863+
c.resty.OnResponseLog(func(r *resty.ResponseLog) error {
864+
redactLogHeaders(r.Header)
834865
return nil
835866
})
836867

client_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/google/go-cmp/cmp"
1818
"github.com/jarcoal/httpmock"
1919
"github.com/linode/linodego/internal/testutil"
20+
"github.com/stretchr/testify/require"
2021
)
2122

2223
func TestClient_SetAPIVersion(t *testing.T) {
@@ -703,3 +704,101 @@ func TestMonitorClient_SetAPIBasics(t *testing.T) {
703704
t.Fatal(cmp.Diff(client.resty.BaseURL, expectedHost))
704705
}
705706
}
707+
708+
func TestRedactHeaders(t *testing.T) {
709+
tests := []struct {
710+
name string
711+
headers http.Header
712+
wantVal map[string]string
713+
}{
714+
{
715+
name: "redacts authorization header",
716+
headers: http.Header{
717+
"Authorization": []string{"Bearer supersecrettoken"},
718+
"Content-Type": []string{"application/json"},
719+
},
720+
wantVal: map[string]string{
721+
"Authorization": redactHeadersMap["Authorization"],
722+
"Content-Type": "application/json",
723+
},
724+
},
725+
{
726+
name: "leaves non-sensitive headers unchanged",
727+
headers: http.Header{
728+
"Content-Type": []string{"application/json"},
729+
"Accept": []string{"application/json"},
730+
},
731+
wantVal: map[string]string{
732+
"Content-Type": "application/json",
733+
"Accept": "application/json",
734+
},
735+
},
736+
{
737+
name: "handles empty headers",
738+
headers: http.Header{},
739+
wantVal: map[string]string{},
740+
},
741+
{
742+
name: "does not mutate original headers",
743+
headers: http.Header{
744+
"Authorization": []string{"Bearer supersecrettoken"},
745+
},
746+
wantVal: map[string]string{
747+
"Authorization": redactHeadersMap["Authorization"],
748+
},
749+
},
750+
}
751+
752+
for _, tt := range tests {
753+
t.Run(tt.name, func(t *testing.T) {
754+
originalAuth := tt.headers.Get("Authorization")
755+
756+
result := redactHeaders(tt.headers)
757+
758+
// Verify expected values in result
759+
for key, expectedVal := range tt.wantVal {
760+
if got := result.Get(key); got != expectedVal {
761+
t.Errorf("redactHeaders() header %q = %q, want %q", key, got, expectedVal)
762+
}
763+
}
764+
765+
// Verify original was not mutated
766+
if tt.headers.Get("Authorization") != originalAuth {
767+
t.Error("redactHeaders() mutated the original headers")
768+
}
769+
})
770+
}
771+
}
772+
773+
func TestEnableLogSanitization(t *testing.T) {
774+
mockClient := testutil.CreateMockClient(t, NewClient)
775+
mockClient.SetDebug(true)
776+
777+
plainTextToken := "supersecrettoken"
778+
mockClient.SetToken(plainTextToken)
779+
780+
var logBuf bytes.Buffer
781+
logger := testutil.CreateLogger()
782+
logger.L.SetOutput(&logBuf)
783+
mockClient.SetLogger(logger)
784+
785+
httpmock.RegisterResponder("GET", "=~.*",
786+
httpmock.NewStringResponder(200, `{}`).HeaderSet(http.Header{
787+
"Authorization": []string{"Bearer " + plainTextToken},
788+
}))
789+
790+
_, err := mockClient.resty.R().Get("https://api.linode.com/v4/test")
791+
require.NoError(t, err)
792+
793+
logOutput := logBuf.String()
794+
795+
// Verify token is not present in either request or response logs
796+
if strings.Contains(logOutput, plainTextToken) {
797+
t.Errorf("log output contains raw token %q, expected it to be redacted", plainTextToken)
798+
}
799+
800+
// Verify Authorization header still appears (as redacted value) in request log
801+
if !strings.Contains(logOutput, "Authorization") {
802+
t.Error("expected Authorization header to appear in request log output")
803+
}
804+
}

0 commit comments

Comments
 (0)