Repository: getsentry/sentry-go

Issue:
PII opt-in by default
We need to add to the SDK a new config bool such that anything that we know will identify a user is not sent on events by default. The entire user object essentially, auth headers, IP addresses, that sort of thing.

See `send_default_pii` in the Python SDK.
https://docs.sentry.io/error-reporting/configuration/?platform=python#send-default-pii

The current docs for Go suggests that it exists as `SendDefaultPii`, but it doesn't exist yet.
https://docs.sentry.io/error-reporting/configuration/?platform=go#send-default-pii

(see also https://github.com/getsentry/develop/issues/466)

Discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9843f9..ec59d30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,11 @@
 # Changelog
 
+## Unreleased
+
 - fix: Scope values should not override Event values (#446)
 - feat: Extend User inteface by adding Data, Name and Segment (#483)
 - feat: Make maximum amount of spans configurable (#460)
+- feat: Add ClientOptions.SendDefaultPii #485
 
 ## 0.14.0
 
diff --git a/client.go b/client.go
index be8d266..dd3f720 100644
--- a/client.go
+++ b/client.go
@@ -134,6 +134,9 @@ type ClientOptions struct {
 	// and if applicable, caught errors type and value.
 	// If the match is found, then a whole event will be dropped.
 	IgnoreErrors []string
+	// If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
+	// By default, no such data is sent.
+	SendDefaultPii bool
 	// BeforeSend is called before error events are sent to Sentry.
 	// Use it to mutate the event or return nil to discard the event.
 	// See EventProcessor if you need to mutate transactions.
diff --git a/interfaces.go b/interfaces.go
index e1b8b46..72e566a 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -28,6 +28,15 @@ const (
 	LevelFatal   Level = "fatal"
 )
 
+func getSensitiveHeaders() map[string]bool {
+	return map[string]bool{
+		"Authorization":   true,
+		"Cookie":          true,
+		"X-Forwarded-For": true,
+		"X-Real-Ip":       true,
+	}
+}
+
 // SdkInfo contains all metadata about about the SDK being used.
 type SdkInfo struct {
 	Name         string       `json:"name,omitempty"`
@@ -156,23 +165,37 @@ func NewRequest(r *http.Request) *Request {
 	}
 	url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
 
-	// We read only the first Cookie header because of the specification:
-	// https://tools.ietf.org/html/rfc6265#section-5.4
-	// When the user agent generates an HTTP request, the user agent MUST NOT
-	// attach more than one Cookie header field.
-	cookies := r.Header.Get("Cookie")
-
-	headers := make(map[string]string, len(r.Header))
-	for k, v := range r.Header {
-		headers[k] = strings.Join(v, ",")
-	}
-	headers["Host"] = r.Host
-
+	var cookies string
 	var env map[string]string
-	if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
-		env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
+	headers := map[string]string{}
+
+	if client := CurrentHub().Client(); client != nil {
+		if client.Options().SendDefaultPii {
+			// We read only the first Cookie header because of the specification:
+			// https://tools.ietf.org/html/rfc6265#section-5.4
+			// When the user agent generates an HTTP request, the user agent MUST NOT
+			// attach more than one Cookie header field.
+			cookies = r.Header.Get("Cookie")
+
+			for k, v := range r.Header {
+				headers[k] = strings.Join(v, ",")
+			}
+
+			if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
+				env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
+			}
+		}
+	} else {
+		sensitiveHeaders := getSensitiveHeaders()
+		for k, v := range r.Header {
+			if _, ok := sensitiveHeaders[k]; !ok {
+				headers[k] = strings.Join(v, ",")
+			}
+		}
 	}
 
+	headers["Host"] = r.Host
+
 	return &Request{
 		URL:         url,
 		Method:      r.Method,



Test patch:
diff --git a/fasthttp/sentryfasthttp_test.go b/fasthttp/sentryfasthttp_test.go
index 0fbdbf6..cac7fae 100644
--- a/fasthttp/sentryfasthttp_test.go
+++ b/fasthttp/sentryfasthttp_test.go
@@ -142,6 +142,7 @@ func TestIntegration(t *testing.T) {
 
 	eventsCh := make(chan *sentry.Event, len(tests))
 	err := sentry.Init(sentry.ClientOptions{
+		SendDefaultPii: true,
 		BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
 			eventsCh <- event
 			return event
diff --git a/http/sentryhttp_test.go b/http/sentryhttp_test.go
index 5a9ea53..898e337 100644
--- a/http/sentryhttp_test.go
+++ b/http/sentryhttp_test.go
@@ -156,6 +156,7 @@ func TestIntegration(t *testing.T) {
 
 	eventsCh := make(chan *sentry.Event, len(tests))
 	err := sentry.Init(sentry.ClientOptions{
+		SendDefaultPii: true,
 		BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
 			eventsCh <- event
 			return event
diff --git a/interfaces_test.go b/interfaces_test.go
index ca48ae8..a232d61 100644
--- a/interfaces_test.go
+++ b/interfaces_test.go
@@ -66,16 +66,34 @@ func TestUserMarshalJson(t *testing.T) {
 }
 
 func TestNewRequest(t *testing.T) {
+	currentHub.BindClient(&Client{
+		options: ClientOptions{
+			SendDefaultPii: true,
+		},
+	})
+	// Unbind the client afterwards, to not affect other tests
+	defer currentHub.stackTop().SetClient(nil)
+
 	const payload = `{"test_data": true}`
-	got := NewRequest(httptest.NewRequest("POST", "/test/?q=sentry", strings.NewReader(payload)))
+	r := httptest.NewRequest("POST", "/test/?q=sentry", strings.NewReader(payload))
+	r.Header.Add("Authorization", "Bearer 1234567890")
+	r.Header.Add("Cookie", "foo=bar")
+	r.Header.Add("X-Forwarded-For", "127.0.0.1")
+	r.Header.Add("X-Real-Ip", "127.0.0.1")
+
+	got := NewRequest(r)
 	want := &Request{
 		URL:         "http://example.com/test/",
 		Method:      "POST",
 		Data:        "",
 		QueryString: "q=sentry",
-		Cookies:     "",
+		Cookies:     "foo=bar",
 		Headers: map[string]string{
-			"Host": "example.com",
+			"Authorization":   "Bearer 1234567890",
+			"Cookie":          "foo=bar",
+			"Host":            "example.com",
+			"X-Forwarded-For": "127.0.0.1",
+			"X-Real-Ip":       "127.0.0.1",
 		},
 		Env: map[string]string{
 			"REMOTE_ADDR": "192.0.2.1",
@@ -87,6 +105,31 @@ func TestNewRequest(t *testing.T) {
 	}
 }
 
+func TestNewRequestWithNoPii(t *testing.T) {
+	const payload = `{"test_data": true}`
+	r := httptest.NewRequest("POST", "/test/?q=sentry", strings.NewReader(payload))
+	r.Header.Add("Authorization", "Bearer 1234567890")
+	r.Header.Add("Cookie", "foo=bar")
+	r.Header.Add("X-Forwarded-For", "127.0.0.1")
+	r.Header.Add("X-Real-Ip", "127.0.0.1")
+
+	got := NewRequest(r)
+	want := &Request{
+		URL:         "http://example.com/test/",
+		Method:      "POST",
+		Data:        "",
+		QueryString: "q=sentry",
+		Cookies:     "",
+		Headers: map[string]string{
+			"Host": "example.com",
+		},
+		Env: nil,
+	}
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("Request mismatch (-want +got):\n%s", diff)
+	}
+}
+
 func TestEventMarshalJSON(t *testing.T) {
 	event := NewEvent()
 	event.Spans = []*Span{{